0%

1. 能源预测(时序)(比赛结束后更新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
#!/usr/bin/env python
# coding: utf-8

# In[ ]:


#!/usr/bin/env python
# coding: utf-8

# In[ ]:

import pandas as pd
import numpy as np
import datetime
import gc
import os

from tqdm import tqdm

import warnings
warnings.filterwarnings('ignore')

import xgboost as xgb
import lightgbm as lgb
from catboost import CatBoostRegressor, Pool

from sklearn.metrics import mean_squared_error
from sklearn.model_selection import StratifiedKFold
# In[ ]:


power_forecast_history_train = pd.read_csv('/opt/dataset/2023“SEED”第四届江苏大数据开发与应用大赛--新能源【复赛B榜】数据集/data/data/train/power_forecast_history.csv')
stub_info_train = pd.read_csv('/opt/dataset/2023“SEED”第四届江苏大数据开发与应用大赛--新能源【复赛B榜】数据集/data/data/train/stub_info.csv')
power_train = pd.read_csv('/opt/dataset/2023“SEED”第四届江苏大数据开发与应用大赛--新能源【复赛B榜】数据集/data/data/train/power.csv')

power_forecast_history_test = pd.read_csv('/opt/dataset/2023“SEED”第四届江苏大数据开发与应用大赛--新能源【复赛B榜】数据集/data/data/test/power_forecast_history.csv')
stub_info_test = pd.read_csv('/opt/dataset/2023“SEED”第四届江苏大数据开发与应用大赛--新能源【复赛B榜】数据集/data/data/test/stub_info.csv')

weather = pd.read_csv('/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/temp_data/city_weather_v2.csv')
h3_feature = pd.read_csv('/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/temp_data/h3_feature_v2.csv')

len_train = len(power_forecast_history_train)

min_id_encode = stub_info_train['id_encode'].min()
max_id_encode = stub_info_train['id_encode'].max()

min_date = power_forecast_history_train['ds'].min()
max_date = power_forecast_history_train['ds'].max()

test_min_date = power_forecast_history_test['ds'].min()
test_max_date = power_forecast_history_test['ds'].max()

# In[ ]:


# power_forecast_history_train = pd.read_csv('../../raw_data/复赛A榜/train/power_forecast_history.csv')
# stub_info_train = pd.read_csv('../../raw_data/复赛A榜/train/stub_info.csv')
# power_train = pd.read_csv('../../raw_data/复赛A榜/train/power.csv')

# power_forecast_history_test = pd.read_csv('../../raw_data/复赛A榜/test/power_forecast_history.csv')

# weather = pd.read_csv('../../raw_data/city_weather_v2.csv')

# h3_feature = pd.read_csv('../../raw_data/h3_feature_v3.csv')


# In[ ]:

# power_forecast_history_train = power_forecast_history_train[power_forecast_history_train['ds'] >= 20220801]
power_forecast_history_train = power_forecast_history_train[power_forecast_history_train.hour.notnull()]


# In[ ]:


convert_dtype_dict = {
'id_encode': np.int16,
'hour': np.int8,
'ele_price': np.float16,
'ser_price': np.float16,
'after_ser_price': np.float16,
'total_price': np.float16,
'f1': np.float16,
'f2': np.float16, ## xgb这里会报错,f2 有inf,需要设置成32
'f3': np.float16,
'ds': np.uint32,
'power': np.float16,
'parking_free': np.uint8,

'flag': np.object,
'h3': np.object,
'ac_equipment_kw': np.uint16,
'dc_equipment_kw': np.uint16,

'city': np.object,
# 'date': np.datetime64,
'temp_max': np.float16,
'temp_min': np.float16,
'weather': np.object,

'province': np.object,
'city': np.object,
'district': np.object,
'town': np.object,
}


# In[ ]:


def convert_type(data, convert_dtype_dict):
cols_dict = dict()
for col in data.columns:
if col in convert_dtype_dict:
cols_dict[col] = convert_dtype_dict[col]

data = data.astype(cols_dict, copy=False)
return data


# In[ ]:


train = power_forecast_history_train.merge(power_train, on=['id_encode', 'ds', 'hour'], how='left')
train = train[train.hour.notnull()]
del power_forecast_history_train, power_train; gc.collect()

train = convert_type(train, convert_dtype_dict)
stub_info_train = convert_type(stub_info_train, convert_dtype_dict)

power_forecast_history_test = convert_type(power_forecast_history_test, convert_dtype_dict)

weather = convert_type(weather, convert_dtype_dict)
h3_feature = convert_type(h3_feature, convert_dtype_dict)


# In[ ]:


data = pd.concat([train, power_forecast_history_test])
data.sort_values(by=['id_encode', 'ds', 'hour'], inplace=True)
del train, power_forecast_history_test; gc.collect()


# In[ ]:


data = data.merge(stub_info_train, on=['id_encode'], how='left')
data = data.merge(h3_feature[['h3', 'province', 'city', 'district', 'town']],
on=['h3'], how='left')

data['date'] = pd.to_datetime(data['ds'].astype(str, copy=False))
weather['date'] = pd.to_datetime(weather['date'].astype(str, copy=False))
data = data.merge(weather[['city', 'date', 'temp_max', 'temp_min', 'weather']], on=['city', 'date'], how='left')

del stub_info_train, h3_feature, weather; gc.collect()


# ## 特征工程

# In[ ]:


data['year'] = data['date'].dt.year.astype(np.uint16, copy=False)
data['quarter'] = data['date'].dt.quarter.astype(np.uint8, copy=False)
data['month'] = data['date'].dt.month.astype(np.uint8, copy=False)
data['day'] = data['date'].dt.day.astype(np.uint8, copy=False)
data['dayofyear'] = data['date'].dt.dayofyear.astype(np.uint16, copy=False)
data['weekofyear'] = data['date'].dt.isocalendar().week.astype(np.uint8, copy=False)
data['dayofweek'] = data['date'].dt.dayofweek.astype(np.uint8, copy=False)
data['is_wknd'] = (data['date'].dt.dayofweek // 6).astype(np.uint8, copy=False)

data['is_month_start'] = data['date'].dt.is_month_start.astype(np.uint8, copy=False)
data['is_month_end'] = data['date'].dt.is_month_end.astype(np.uint8, copy=False)


# In[ ]:


def f3_disceret(x):
if x < 0.6:
return 0
elif x < 0.7:
return 1
elif x < 0.8:
return 2
elif x < 0.9:
return 3
elif x < 1:
return 4
else:
return 5
data['f3_dis'] = data['f3'].apply(lambda x:f3_disceret(x)).astype(np.uint8, copy=False)


# In[ ]:


def total_price_disceret(x):
if x < 0.75:
return 0
elif x < 1.2:
return 1
elif x < 1.5:
return 2
else:
return 3
data['total_price_dis'] = data['total_price'].apply(lambda x:total_price_disceret(x)).astype(np.uint8, copy=False)


# In[ ]:


def f1_disceret(x):
if x < 20:
return 0
elif x < 40:
return 1
elif x < 60:
return 2
elif x < 80:
return 3
elif x < 100:
return 4
elif x < 150:
return 5
else:
return 6
data['f1_dis'] = data['f1'].apply(lambda x:f1_disceret(x)).astype(np.uint8, copy=False)


# In[ ]:


def after_ser_price_disceret(x):
if x < 0.2:
return 0
elif x < 0.4:
return 1
elif x < 0.6:
return 2
else:
return 3
data['after_ser_price_dis'] = data['after_ser_price'].apply(lambda x:after_ser_price_disceret(x)).astype(np.uint8, copy=False)


# In[ ]:


def ser_price_disceret(x):
if x >= 0 and x < 0.25:
return 0
elif x < 0.35:
return 1
elif x < 0.5:
return 2
elif x < 0.65:
return 3
elif x < 1:
return 4
else:
return 5
data['ser_price_dis'] = data['ser_price'].apply(lambda x:ser_price_disceret(x)).astype(np.uint8, copy=False)


# In[ ]:


def ele_price_disceret(x):
if x == 0:
return 0
elif x > 0 and x < 0.5:
return 1
elif x >= 0.5 and x < 0.75:
return 2
elif x == 0.75:
return 3
elif x > 0.75 and x < 1:
return 4
elif x >= 1:
return 5
data['ele_price_dis'] = data['ele_price'].apply(lambda x:ele_price_disceret(x)).astype(np.uint8, copy=False)


# In[ ]:


for feat in [
'flag', 'h3',
'province', 'city', 'district', 'town', 'weather']:
if data[feat].isnull().sum() > 0:
print(feat, data[feat].isnull().sum())
data[feat] = data[feat].fillna(-1)
unique = list(data[feat].unique())
value = list(range(data[feat].nunique()))
le = dict(zip(unique, value))
data[feat] = data[feat].map(le).astype(np.uint8, copy=False)


# In[ ]:


def count_0(x):
return (x == 0).sum()

agg_ = data.groupby(['id_encode'])['power'].agg(count_0).reset_index()
agg_.columns = ['id_encode', 'power_count_0']
agg_['power_count_0'] = agg_['power_count_0'].astype(np.uint16, copy=False)
data = data.merge(agg_, on='id_encode', how='left')
del agg_; gc.collect()


# In[ ]:


### count
for feat in [
'id_encode',

'parking_free','dc_equipment_kw',
# 'town', 'ad_code',
# 'temp_max', 'temp_min',
# 'weather',
# 'year',
# 'quarter',
# 'month', 'day', 'dayofyear', 'weekofyear', 'dayofweek', 'is_wknd',
# 'is_month_start', 'is_month_end',

'ele_price_dis',
# 'ser_price_dis',
# 'after_ser_price_dis',
# 'f1_dis',
'total_price_dis',

'f3_dis'
]:
agg_ = data.groupby([feat])['power'].agg('count').reset_index()
agg_.columns = [feat, f'{feat}_count']
agg_[f'{feat}_count'] = agg_[f'{feat}_count'].astype(np.uint32, copy=False)
data = data.merge(agg_, on=feat, how='left')
del agg_; gc.collect()


# In[ ]:


for feat in [
'temp_max',
'temp_min', 'weather','ele_price_dis', 'ser_price_dis',
'after_ser_price_dis', 'f1_dis', 'total_price_dis'
]:
agg_ = data.groupby(['id_encode'])[feat].agg('nunique').reset_index()
agg_.columns = ['id_encode', f'id_encode_{feat}_nunique']
agg_[f'id_encode_{feat}_nunique'] = agg_[f'id_encode_{feat}_nunique'].astype(np.uint8, copy=False)
data = data.merge(agg_, on='id_encode', how='left')
del agg_; gc.collect()


# In[ ]:


# 目标编码
for feat in ['id_encode', 'parking_free', 'flag',
'h3', 'year','quarter',
'month', 'day', 'hour',
'dayofweek', 'is_wknd',

'dc_equipment_kw',
'city',
# 'province', 'district', 'town', 'ad_code', 'temp_max', 'temp_min',
'weather',
'ele_price_dis', 'ser_price_dis', 'after_ser_price_dis', 'f1_dis', 'total_price_dis',

'f3_dis'

]:
agg_ = data.groupby(feat).agg({
'power': ['mean', 'max', 'min', 'std']
})
new_col_names = [feat + '_' + f[0] + '_' + f[1] for f in agg_.columns]
agg_.columns = new_col_names

agg_[new_col_names] = agg_[new_col_names].astype(np.float16, copy=False)
agg_ = agg_.reset_index(drop = False)
data = data.merge(agg_, on = feat, how = 'left')

for feat1 in [
# 'id_encode',
'parking_free', 'flag', 'dc_equipment_kw',
'ele_price_dis', 'ser_price_dis', 'after_ser_price_dis', 'f1_dis', 'total_price_dis',
'f3_dis'
]:
for feat2 in [
'year',
'quarter',
'month',
'day',
'hour',
'dayofweek',
'is_wknd'
]:
agg_ = data.groupby([feat1, feat2]).agg({
'power': ['mean', 'max', 'min', 'std']
})
new_col_names = [feat1 + '_' + feat2 + '_' + f[0] + '_' + f[1] for f in agg_.columns]
agg_.columns = new_col_names

agg_[new_col_names] = agg_[new_col_names].astype(np.float16, copy=False)
agg_ = agg_.reset_index(drop = False)
data = data.merge(agg_, on = [feat1, feat2], how = 'left')
del agg_; gc.collect()


# In[ ]:


for feat1 in [
'id_encode',
# 'h3'
# 'parking_free', 'flag', 'h3'
]:
for feat2 in [
# 'year',
# 'quarter',
'month',
'day',
'dayofweek',
'hour',
'is_wknd'
]:
agg_ = data.groupby([feat1, feat2]).agg({
'power': ['mean', 'max', 'min', 'std']
})
new_col_names = [feat1 + '_' + feat2 + '_' + f[0] + '_' + f[1] for f in agg_.columns]
agg_.columns = new_col_names
agg_[new_col_names] = agg_[new_col_names].astype(np.float16, copy=False)
agg_ = agg_.reset_index(drop = False)
data = data.merge(agg_, on = [feat1, feat2], how = 'left')
del agg_; gc.collect()


# In[ ]:


for feat in data.columns:
if feat == 'year': continue
if data[feat].nunique() == 1:
# print(feat)
del data[feat]

for feat in data.columns:
try:
if (data[feat].max() > 999999):
print(feat, data[feat].min(),data[feat].max())
except:
print(feat)


# In[ ]:


data.info()


# ## shift 特征

# In[ ]:


def get_feature(data, lag=1):

tmp_cols_3 = []

for shift in range(lag * 24, (lag + 3) * 24):
column = f'power_shift{shift - lag * 24}'
data[column]=data.groupby('id_encode')['power'].shift(shift).astype(np.float16, copy=False)

tmp_cols_3.append(column)


diff_cols = [f"{feat}_diff" for feat in tmp_cols_3[1:]]
data[diff_cols] = data[tmp_cols_3].diff(axis=1).astype(np.float16, copy=False)[tmp_cols_3[1:]]

data[f'diff_mean_1_to_3'] = data[diff_cols].mean(axis=1).astype(np.float16, copy=False)

data[f'power_window_1_to_3_mean'] = data[tmp_cols_3].mean(axis=1).astype(np.float16, copy=False)
data[f'power_window_1_to_3_max'] = data[tmp_cols_3].max(axis=1).astype(np.float16, copy=False)
data[f'power_window_1_to_3_min'] = data[tmp_cols_3].min(axis=1).astype(np.float16, copy=False)
data[f'power_window_1_to_3_median'] = data[tmp_cols_3].min(axis=1).astype(np.float16, copy=False)
data[f'power_window_1_to_3_std'] = data[tmp_cols_3].std(axis=1).astype(np.float16, copy=False)

return data


# In[ ]:


def get_history_info(df_label, history, window):
id_encode_count_0 = history.groupby(['id_encode'])['power'].agg(count_0).reset_index()
id_encode_count_0.columns = ['id_encode', f'power_count_0_window_{window}']
id_encode_count_0[f'power_count_0_window_{window}'] = id_encode_count_0[f'power_count_0_window_{window}'].astype(np.uint16, copy=False)

df_label = df_label.merge(id_encode_count_0, on=['id_encode'], how='left')

## count/nunique
for feat in [
'id_encode',
'parking_free',
'dc_equipment_kw',
'ele_price_dis',
'total_price_dis',
'f3_dis'
]:

agg_ = history[feat].value_counts().reset_index()
agg_.columns = [feat, f'{feat}_count_window_{window}']
agg_[f'{feat}_count_window_{window}'] = agg_[f'{feat}_count_window_{window}'].astype(np.uint32, copy=False)
df_label = df_label.merge(agg_, on=[feat], how='left')

for feat in [
'temp_max',
'temp_min', 'weather','ele_price_dis', 'ser_price_dis',
'after_ser_price_dis', 'f1_dis', 'total_price_dis'
]:
agg_ = history.groupby(['id_encode'])[feat].agg('nunique').reset_index()
agg_.columns = ['id_encode', f'id_encode_{feat}_nunique_window_{window}']
agg_[f'id_encode_{feat}_nunique_window_{window}'] = agg_[f'id_encode_{feat}_nunique_window_{window}'].astype(np.uint8, copy=False)

df_label = df_label.merge(agg_, on=['id_encode'], how='left')

## 目标编码
for feat in ['id_encode', 'parking_free', 'flag',
'h3', 'year','quarter',
'month', 'day', 'hour',
'dayofweek', 'is_wknd',

'dc_equipment_kw',
'ele_price_dis', 'ser_price_dis', 'after_ser_price_dis', 'f1_dis', 'total_price_dis',

'f3_dis'

]:
agg_ = history.groupby([feat])['power'].agg('mean').reset_index()
agg_.columns = [feat, f'{feat}_power_mean_window_{window}']
agg_[f'{feat}_power_mean_window_{window}'] = agg_[f'{feat}_power_mean_window_{window}'].astype(np.float16, copy=False)
df_label = df_label.merge(agg_, on=[feat], how='left')


for feat1 in [
'id_encode',
'parking_free', 'flag', 'dc_equipment_kw',
'ele_price_dis', 'ser_price_dis', 'after_ser_price_dis', 'f1_dis', 'total_price_dis',
'f3_dis'
]:
for feat2 in [
# 'year',
# 'quarter',
# 'month',
# 'day',
'hour',
'dayofweek',
'is_wknd'
]:
agg_ = history.groupby([feat1, feat2])['power'].agg('mean').reset_index()
agg_.columns = [feat1, feat2, f'{feat1}_{feat2}_power_mean_window_{window}']
agg_[f'{feat1}_{feat2}_power_mean_window_{window}'] = agg_[f'{feat1}_{feat2}_power_mean_window_{window}'].astype(np.float16, copy=False)
df_label = df_label.merge(agg_, on=[feat1, feat2], how='left')

return df_label


# In[ ]:


drop_features = ['ds',
'power',
'date',
'id_encode_year_count',
'id_encode_quarter_count',
'id_encode_month_count',
'id_encode_day_count',
'id_encode_dayofweek_count',
'parking_free_id_encode_nunique',
'dc_equipment_kw_id_encode_nunique',
'city_power_mean',
'city_power_max',
'city_power_std',
'weather_power_mean',
'weather_power_max',
'weather_power_std',
'parking_free_year_power_mean',
'parking_free_year_power_max',
'parking_free_year_power_std',
'id_encode_month_power_mean',
'id_encode_month_power_max',
'id_encode_month_power_min',
'id_encode_month_power_std',
'id_encode_day_power_mean',
'id_encode_day_power_max',
'id_encode_day_power_min',
'id_encode_day_power_std'
]


# In[ ]:


cat_features = [
'id_encode', 'parking_free', 'flag', 'h3','province', 'city', 'district', 'town',
'weather', 'is_wknd', 'is_month_start', 'is_month_end'
]


# In[ ]:


def cat_train(train_df, features):

X_train = train_df[features]
Y_train = train_df[['power']]
scores = []

folds = StratifiedKFold(n_splits= 5, shuffle=True, random_state=1996)
for fold_, (train_index, test_index) in enumerate(folds.split(train_df, train_df['id_encode'])):
print('Fold_{}'.format(fold_))

X_trn, y_trn = X_train.iloc[train_index], Y_train.iloc[train_index]
X_val, y_val = X_train.iloc[test_index], Y_train.iloc[test_index]

trn_dataset = Pool(X_trn,y_trn)
val_dataset = Pool(X_val,y_val)

best_n = 3500
model = CatBoostRegressor(iterations=best_n,
learning_rate=0.1,
depth=6,
loss_function='RMSE',
eval_metric='RMSE',
random_seed = 22222,
# bagging_temperature = 0.2,
od_type='Iter',
metric_period = 500,
# od_wait=500,
# task_type='GPU',
l2_leaf_reg=5,
min_data_in_leaf=500,
# scale_pos_weight=16,
)

model.fit(trn_dataset,
eval_set=val_dataset,
use_best_model=True,
verbose=500,
early_stopping_rounds=50)

model.save_model(f'/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/model/cat_test{fold_}.txt')

val_pred = model.predict(X_val)

score = mean_squared_error(y_val, val_pred, squared=False)
scores.append(score)
print('===rmse===', score)

del X_trn, y_trn, X_val, y_val, trn_dataset, val_dataset, model, val_pred; gc.collect()

del train_df; gc.collect()
score = np.mean(scores)
print('mean rmse:', score)

def lgb_train(train_df, features):
X_train = train_df[features]
Y_train = train_df[['power']]

scores = []
params = {'learning_rate': 0.1,
'boosting_type': 'gbdt',
'objective': 'rmse',
'metric': 'rmse',
'min_child_samples': 46,
'min_child_weight': 0.01,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 5,
'num_leaves': 32,
'min_data_in_leaf': 30,
# 'max_depth': 7,
'n_jobs': -1,
'seed': 22222,
'verbosity': -1,
}

folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=1996)
for fold_, (train_index, test_index) in enumerate(folds.split(train_df, train_df['id_encode'])):
print('Fold_{}'.format(fold_))

X_trn, y_trn = X_train.iloc[train_index], Y_train.iloc[train_index]
X_val, y_val = X_train.iloc[test_index], Y_train.iloc[test_index]

trn_data = lgb.Dataset(X_trn,y_trn)
val_data = lgb.Dataset(X_val,y_val)

best_n = 3000
print('best_n:',best_n)
model = lgb.train(params,
trn_data,
best_n,
valid_sets = [trn_data, val_data],
verbose_eval = 500,
early_stopping_rounds = 50)

model.save_model(f'/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/model/lgb_test{fold_}.txt')
val_pred = model.predict(X_val)

score = mean_squared_error(y_val, val_pred, squared=False)
scores.append(score)
print('===rmse===', score)

del X_trn, y_trn, X_val, y_val, trn_data, val_data, model, val_pred; gc.collect()

del train_df; gc.collect()
score = np.mean(scores)
print('mean rmse:', score)

def xgb_train(train_df, features):
X_train = train_df[features]
Y_train = train_df[['power']]

scores = []

folds = StratifiedKFold(n_splits=5, shuffle=True, random_state=1996)
for fold_, (train_index, test_index) in enumerate(folds.split(train_df, train_df['id_encode'])):
print('Fold_{}'.format(fold_))

X_trn, y_trn = X_train.iloc[train_index], Y_train.iloc[train_index]
X_val, y_val = X_train.iloc[test_index], Y_train.iloc[test_index]

best_n = 3000
model = xgb.XGBRegressor(base_score=0.5, booster='gbtree',
n_estimators=best_n,
early_stopping_rounds=50,
objective='reg:squarederror',
max_depth=7,
# tree_method='gpu_hist',
learning_rate=0.01)

model.fit(X_trn, y_trn,
eval_set=[(X_trn, y_trn), (X_val,y_val)],
verbose=500)

model.save_model(f'/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/model/xgb_test{fold_}.txt')

val_pred = model.predict(X_val)

score = mean_squared_error(y_val, val_pred, squared=False)
scores.append(score)
print('===rmse===', score)

del X_trn, y_trn, X_val, y_val, model, val_pred; gc.collect()
del train_df; gc.collect()
score = np.mean(scores)
print('mean rmse:', score)

def train(data, date_id, lag, drop_features):

data = get_feature(data, lag=lag)

train_df = data[data['ds'] <= date_id].reset_index(drop = True)

if lag == 1:
train_dfs = []
for ds in tqdm(sorted(train_df['ds'].unique(), reverse=True)[:-14]):
df_label = data[data['ds'] == ds]

if ds % 10 < 5:
for window in [7, 14]:
end_time = datetime.datetime.strptime(str(ds),"%Y%m%d")
start_time = end_time - datetime.timedelta(window)
history = data[(data['date'] < end_time) & (data['date'] >= start_time)]
df_label = get_history_info(df_label, history, window)
train_dfs.append(df_label)
else:
for window in [7, 14]:
end_time = datetime.datetime.strptime(str(ds),"%Y%m%d")
start_time = end_time - datetime.timedelta(window)
history = data[(data['date'] < end_time) & (data['date'] >= start_time)]
df_label = get_history_info(df_label, history, window)
train_dfs.append(df_label)

del df_label, history; gc.collect()
train_df = pd.concat(train_dfs)

for feat in train_df.columns:
if str(train_df[feat].dtype) == 'float64':
if train_df[feat].max() < np.finfo(np.float16).max:
train_df[feat] = train_df[feat].astype(np.float16, copy=False)
else:
train_df[feat] = train_df[feat].astype(np.float32, copy=False)

del data, train_dfs; gc.collect()

drop = []
for feat in train_df.columns:
if train_df[feat].nunique() == 1:
del train_df[feat]
gc.collect()

features = [feat for feat in train_df.columns if feat not in drop_features]

for feat in train_df.columns:
if feat not in features + ['power', 'ds']:
del train_df[feat]

for feat in features + ['power', 'ds']:
try:
if (train_df[feat].max() > 999999):
print(feat, train_df[feat].min(),train_df[feat].max())
except:
print(feat)


# gc.collect()
cat_train(train_df, features)
# train_df = train_df[train_df['ds'] >= 20220801]
# gc.collect()
# lgb_train(train_df, features)
# train_df.replace([np.inf, -np.inf], np.nan, inplace=True)
# xgb_train(train_df, features)

print(len(features), train_df.shape)
return features


# In[ ]:

def predict(model, data, pred_date_id, lag, features, merge=False, mode='cat'):
data = get_feature(data, lag=lag)

test_df = data[data['ds'] == pred_date_id]

if lag == 1:
for window in [7, 14]:
end_time = datetime.datetime.strptime('20230415',"%Y%m%d")
start_time = end_time - datetime.timedelta(window)
history = data[(data['date'] < end_time) & (data['date'] >= start_time)]

test_df = get_history_info(test_df, history, window)

# print(len(features), test_df.shape)


if mode == 'xgb':
test_df.replace([np.inf, -np.inf], np.nan, inplace=True)
pred = model.predict(xgb.DMatrix(test_df[features]))
else:
pred = model.predict(test_df[features])

test_df['power'] = pred

if merge == True:
data1 = data[data['ds'] != pred_date_id]
data2 = data[data['ds'] == pred_date_id]

del data2['power']
data2 = data2.merge(test_df[['id_encode', 'ds', 'hour', 'power']],
on=['id_encode', 'ds', 'hour'], how='left')

data = pd.concat([data1, data2])

data = data.sort_values(['id_encode', 'ds', 'hour']).reset_index(drop=True)

return data, pred


# ### model1

# In[ ]:

len_use = len(data)
features = train(data, 20230414, 1, drop_features)


# In[ ]:


cat_res = []
for fold_ in range(5):
model1 = CatBoostRegressor()
model1.load_model(f'/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/model/cat_test{fold_}.txt')
print(f'load model: /opt/project/workspace/wdbMYCgvUKxijgaxPNUL/model/cat_test{fold_}.txt')
data_fold = data.copy()

data_fold, model1_pred_20230415 = predict(model1, data_fold, 20230415, 1, features, merge=True, mode='cat')
data_fold, model1_pred_20230416 = predict(model1, data_fold, 20230416, 1, features, merge=True, mode='cat')
data_fold, model1_pred_20230417 = predict(model1, data_fold, 20230417, 1, features, merge=True, mode='cat')
data_fold, model1_pred_20230418 = predict(model1, data_fold, 20230418, 1, features, merge=True, mode='cat')
data_fold, model1_pred_20230419 = predict(model1, data_fold, 20230419, 1, features, merge=True, mode='cat')
data_fold, model1_pred_20230420 = predict(model1, data_fold, 20230420, 1, features, merge=True, mode='cat')
data_fold, model1_pred_20230421 = predict(model1, data_fold, 20230421, 1, features, merge=True, mode='cat')

test_df = data_fold[data_fold['ds'] > 20230414].reset_index(drop = True)

test_df['power'] = test_df['power'].apply(lambda x : 0 if x<0 else x)

submit = test_df[['id_encode', 'date','hour','power']]
submit['time'] = submit['date'].astype(str)
submit['time'] = submit['time'].apply(lambda x: x.replace('-', ''))
submit.rename(columns={'time':'ds'}, inplace=True)

submit[['id_encode', 'ds', 'hour','power']].to_csv(f'/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/result/cat_fold{fold_}.csv', index=None)
cat_res.append(submit[['id_encode', 'ds', 'hour', 'power']])

# lgb_res = []
# for fold_ in range(5):
# model1 = lgb.Booster(model_file=f'/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/model/lgb_test{fold_}.txt')
# print(f'load model: /opt/project/workspace/wdbMYCgvUKxijgaxPNUL/model/lgb_test{fold_}.txt')
# data_fold = data.copy()

# data_fold, model1_pred_20230415 = predict(model1, data_fold, 20230415, 1, features, merge=True, mode='lgb')
# data_fold, model1_pred_20230416 = predict(model1, data_fold, 20230416, 1, features, merge=True, mode='lgb')
# data_fold, model1_pred_20230417 = predict(model1, data_fold, 20230417, 1, features, merge=True, mode='lgb')
# data_fold, model1_pred_20230418 = predict(model1, data_fold, 20230418, 1, features, merge=True, mode='lgb')
# data_fold, model1_pred_20230419 = predict(model1, data_fold, 20230419, 1, features, merge=True, mode='lgb')
# data_fold, model1_pred_20230420 = predict(model1, data_fold, 20230420, 1, features, merge=True, mode='lgb')
# data_fold, model1_pred_20230421 = predict(model1, data_fold, 20230421, 1, features, merge=True, mode='lgb')

# test_df = data_fold[data_fold['ds'] > 20230414].reset_index(drop = True)

# test_df['power'] = test_df['power'].apply(lambda x : 0 if x<0 else x)

# submit = test_df[['id_encode', 'date', 'hour','power']]
# submit['time'] = submit['date'].astype(str)
# submit['time'] = submit['time'].apply(lambda x: x.replace('-', ''))
# submit.rename(columns={'time':'ds'}, inplace=True)
# submit[['id_encode', 'ds', 'hour', 'power']].to_csv(f'/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/result/lgb_test{fold_}.csv', index=None)
# del data_fold; gc.collect()
# lgb_res.append(submit[['id_encode', 'ds', 'hour', 'power']])


# xgb_res = []
# for fold_ in range(5):
# model1 = xgb.Booster()
# model1.load_model(f'/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/model/xgb_test{fold_}.txt')
# # print(f'load model: /opt/project/workspace/wdbMYCgvUKxijgaxPNUL/model/xgb_test{fold_}.txt')
# data_fold = data.copy()

# data_fold, model1_pred_20230415 = predict(model1, data_fold, 20230415, 1, features, merge=True, mode='xgb')
# data_fold, model1_pred_20230416 = predict(model1, data_fold, 20230416, 1, features, merge=True, mode='xgb')
# data_fold, model1_pred_20230417 = predict(model1, data_fold, 20230417, 1, features, merge=True, mode='xgb')
# data_fold, model1_pred_20230418 = predict(model1, data_fold, 20230418, 1, features, merge=True, mode='xgb')
# data_fold, model1_pred_20230419 = predict(model1, data_fold, 20230419, 1, features, merge=True, mode='xgb')
# data_fold, model1_pred_20230420 = predict(model1, data_fold, 20230420, 1, features, merge=True, mode='xgb')
# data_fold, model1_pred_20230421 = predict(model1, data_fold, 20230421, 1, features, merge=True, mode='xgb')

# test_df = data_fold[data_fold['ds'] > 20230414].reset_index(drop = True)

# test_df['power'] = test_df['power'].apply(lambda x : 0 if x<0 else x)

# submit = test_df[['id_encode', 'date', 'hour','power']]
# submit['time'] = submit['date'].astype(str)
# submit['time'] = submit['time'].apply(lambda x: x.replace('-', ''))
# submit.rename(columns={
# 'time':'ds'
# }, inplace=True)
# submit[['id_encode', 'ds', 'hour', 'power']].to_csv(f'/opt/project/workspace/wdbMYCgvUKxijgaxPNUL/result/xgb_test{fold_}.csv', index=None)
# del data_fold; gc.collect()
# xgb_res.append(submit[['id_encode', 'ds', 'hour', 'power']])


# # In[ ]:

cat_5fold_res = cat_res[0]
cat_5fold_res['power'] = (cat_res[0]['power'] +
cat_res[1]['power'] +
cat_res[2]['power'] +
cat_res[3]['power'] +
cat_res[4]['power']
) / 5
# # lgb_5fold_res = lgb_res[0]
# # lgb_5fold_res['power'] = ( lgb_res[0]['power'] +
# # lgb_res[1]['power'] +
# # lgb_res[2]['power'] +
# # lgb_res[3]['power'] +
# # lgb_res[4]['power']
# # ) / 5
# xgb_5fold_res = xgb_res[0]
# xgb_5fold_res['power'] = ( xgb_res[0]['power'] +
# xgb_res[1]['power'] +
# xgb_res[2]['power'] +
# xgb_res[3]['power'] +
# xgb_res[4]['power']
# ) / 5
submit = cat_5fold_res
submit['rank'] = submit['power'].rank(method='first').astype(int)
submit.loc[submit['rank'] >= 15000 , 'power'] *= 1.05
submit.loc[(submit['rank'] < 15000) & (submit['rank'] >= 10000), 'power'] *= 1.03
submit.loc[(submit['rank'] < 10000) & (submit['rank'] >= 5000), 'power'] *= 1.01
submit.loc[(submit['rank'] < 5000), 'power'] *= 1.01

os.makedirs('/opt/output/result', exist_ok=True)

submit[['id_encode','ds','hour', 'power']].to_csv('/opt/output/result/result.csv', index=None)

print('train: ', len_train, 'use: ', len_use)
print('min_id_encode: ', min_id_encode, 'max_id_encode: ', max_id_encode)
print('min_date: ', min_date, 'max_date: ', max_date)
print('test_min_date: ', test_min_date, 'test_max_date: ', test_max_date)

1
2
3
4
5
6
7
8
9
# coding=utf-8
# author: Shuigs18
# date: 2021-04-07

# 基于tensorflow的多层感知机的实现
# 三层(输入、隐藏、输出)MLP + K-fold + weight decay(L2正则化) + dropout
# Relu(隐藏) + softmax(输出)
# 数据集:fashion—mnist
# 梯度计算利用tensorflow
1
2
3
4
5
6
7
import tensorflow as tf
from tensorflow import keras
from matplotlib import pyplot as plt
import numpy as np
import random
import time
from tensorflow.keras.datasets import fashion_mnist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 读取数据并处理(测试集验证集)
# 定义模型参数
# 定义激活函数Relu和softmax
# 定义网络(dropout实现)
# 定义损失函数(加L2正则项 weight decay实现)
# K-fold 函数
# 先 k-fold 确定训练集和验证集
# 然后在将训练集生成SGD训练的迭代器
# train函数 (输入包含训练集迭代器和验证集)
# 小批量梯度下降
# 返回 fold 0,1,2,3,4,5 训练集验证集的误差
# predict函数 生成结果

# 数据处理
(X_train, Y_train), (X_test, Y_test) = fashion_mnist.load_data()
batch_size = 256
X_train = tf.cast(X_train, tf.float32)
X_test = tf.cast(X_test, tf.float32)
X_train = X_train / 255.0 # 颜色的深浅没有关系
X_test = X_test / 255.0
# 划分批次
# train_iter = tf.data.Dataset.from_tensor_slices((x_train, y_train)).batch(batch_size)
1
2
3
4
5
6
# 定义模型参数(一层隐藏层)
dim_inputs, dim_hiddens, dim_outputs = 784, 256, 10
W1 = tf.Variable(tf.random.normal(shape=(dim_inputs, dim_hiddens), mean=0.0, stddev=0.01, dtype=tf.float32))
b1 = tf.Variable(tf.zeros(dim_hiddens, dtype=tf.float32))
W2 = tf.Variable(tf.random.normal(shape=(dim_hiddens, dim_outputs), mean=0.0, stddev=0.01, dtype=tf.float32))
b2 = tf.Variable(tf.random.normal([dim_outputs], mean=0.0, stddev=0.01, dtype=tf.float32))
1
2
3
4
5
6
# 定义激活函数 ReLu softmax
def ReLu(X):
return tf.math.maximum(X, 0)

def softmax(X):
return tf.exp(X) / tf.reduce_sum(tf.math.exp(X), axis=1, keepdims=True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# dropout(H, drop_prob)
# 网络net()
def dropout(H, drop_prob):
assert 0 <= drop_prob <= 1
keep_prob = 1- drop_prob
if keep_prob == 0:
return tf.zeros_like(H)
mask = tf.random.uniform(shape=H.shape, minval=0, maxval=1) < keep_prob
return tf.cast(mask, dtype=tf.float32) * tf.cast(H, dtype=tf.float32) / keep_prob

# 定义整个网络
drop_prob1 = 0.2
def net(X, training=False):
X = tf.reshape(X, shape=(-1, dim_inputs))
H1 = ReLu(tf.matmul(X, W1) + b1)
if training:
H1 = drop_out(H, drop_prob1)
return softmax(tf.matmul(H1, W2) + b2)
1
2
3
4
5
6
# 定义损失函数 交叉熵 L2正则项
def loss_cross_entropy(y_true, y_pred):
return tf.losses.sparse_categorical_crossentropy(y_true, y_pred)

def L2_penalty(W):
return tf.reduce_sum(W ** 2) / 2.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 定义get_K_fold_data函数
def get_K_fold_data(k, i, X, Y):
fold_size = X.shape[0] // k
X_train, Y_train = None, None
for j in range(k):
idx = slice(j * fold_size, (j + 1) * fold_size)
X_part, Y_part = X[idx, :], Y[idx]
if j == i:
X_valid, Y_valid = X_part, Y_part
elif X_train is None:
X_train, Y_train = X_part, Y_part
else:
X_train = tf.concat([X_train, X_part], axis=0)
Y_train = tf.concat([Y_train, Y_part], axis=0)
return X_train, Y_train, X_valid, Y_valid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 定义训练函数
def train(net, train_iter, X_valid, Y_valid, loss, num_epochs, batch_size, params=None, learning_rate=None):
train_loss_sum, valid_loss_sum = 0.0, 0.0
for epoch in range(num_epochs):
train_loss, valid_loss, n = 0.0, 0.0, 0
for X_train, Y_train in train_iter:
with tf.GradientTape() as tape:
Y_pred = net(X_train)
l = loss(Y_train, Y_pred)
# 计算梯度
grads = tape.gradient(l, params)
# 创建一个优化器
opt = tf.keras.optimizers.SGD(learning_rate = learning_rate)
# 梯度下降更新参数(批量梯度下降)
opt.apply_gradients(zip([grad / batch_size for grad in grads], params))
# 更新训练集损失值
train_loss += l.numpy().sum()
n += Y_train.shape[0]
valid_loss += (loss(Y_valid, net(X_valid)).numpy().sum() / Y_valid.shape[0])
train_loss /= n
train_loss_sum += train_loss
valid_loss_sum += valid_loss
train_loss_sum /= num_epochs
valid_loss_sum /= num_epochs

return params, train_loss_sum, valid_loss_sum

def k_fold(k, net, X_train, Y_train, num_epochs,
batch_size, loss_cross_entropy, params=None, learning_rate=None):
start_time = time.time()
for i in range(k):
data = get_K_fold_data(k, i, X_train, Y_train)
train_iter = tf.data.Dataset.from_tensor_slices((data[0], data[1])).batch(batch_size)
X_valid = data[2]
Y_valid = data[3]
params, train_loss, valid_loss = train(net, train_iter, X_valid, Y_valid, loss_cross_entropy, num_epochs, batch_size, params, learning_rate)
print("fold %d: train loss %f, valid loss %f" % (i, train_loss, valid_loss))
end_time = time.time()
print('总用时:%f' % (start_time - end_time))
return params
1
2
3
4
5
# 预测函数 predict()
def predict(net, params, X_test):
Y_pred = net(X_test)
result = tf.argmax(Y_pred, axis=1)
return result
1
2
3
4
5
6
7
8
9
10
11
params = [W1, b1, W2, b2]
num_epochs = 10
params = k_fold(5, net, X_train, Y_train, num_epochs, batch_size, loss_cross_entropy, params, learning_rate=0.1)
'''
fold 0: train loss 0.169400, valid loss 0.169741
fold 1: train loss 0.156253, valid loss 0.167162
fold 2: train loss 0.147749, valid loss 0.159257
fold 3: train loss 0.139382, valid loss 0.157192
fold 4: train loss 0.130207, valid loss 0.149647
总用时:-114.377255
'''
1
2
3
4
5
6
7
8
9
result = predict(net, params, X_test)
'''
<tf.Tensor: shape=(100,), dtype=int64, numpy=
array([9, 2, 1, 1, 6, 1, 4, 6, 5, 7, 4, 5, 5, 3, 4, 1, 2, 2, 8, 0, 2, 5,
7, 5, 1, 4, 6, 0, 9, 6, 8, 8, 3, 3, 8, 0, 7, 5, 7, 9, 0, 1, 6, 7,
6, 7, 2, 1, 2, 6, 4, 2, 5, 8, 2, 2, 8, 4, 8, 0, 7, 7, 8, 5, 1, 1,
3, 3, 7, 8, 7, 0, 2, 6, 2, 3, 1, 2, 8, 4, 1, 8, 5, 9, 5, 0, 3, 2,
0, 2, 5, 3, 6, 7, 1, 8, 0, 1, 2, 2])>
'''

Reference

  • boosting

  • bagging

  • stacking

1. boosting

  • 代表算法:AdaBoost

只能分类?

弱分类器:比较粗糙的分类规则

强分类器:精确的分类规则

提升方法就是组合弱分类器,构成一个强分类器

大多数的提升方法都是改变训练数据的概率分布(训练数据的权值分布),针对不同的训练数据分布调用弱学习算法学习

对于提升方法来说有两个问题需要回答:

  1. 每一轮如何改变训练数据的权值或概率分布

  2. 如何将弱分类器组合成一个强分类器

1.1 AdaBoost

AdaBoost的做法:

针对问题1,提高被前一轮弱分类器错误分类的样本的权值,降低分类正确的样本的权值

针对问题2,组合采用加权多数表决的方法

算法8.1 (AdaBoost)

输入:训练数据集 $T$ ;弱学习算法

输出:最终的分类器 $G(x)$

  1. 初始化训练数据权值分布
    $$
    D_1 = (w_{11}, \cdots, w_{1i}, \cdots, w_{1N}),\quad w_{1i} = \frac{1}{N}, \quad i = 1,2,…,N
    $$

  2. 对 $m = 1,2,…, M$

    a. 使用具有权值分布的 $D_m$ 的训练数据集学习得到基本分类器(学习的依据就是分类误差率最小,分类错误的权重和)
    $$
    G_m(x) : \mathcal{X} \rightarrow \lbrace c_1,c_2,\cdots, c_k \rbrace
    $$
    b.计算 $G_m(x)$ 在训练数据集上的分类误差率
    $$
    e_{m}=\sum_{i=1}^{N} P\left(G_{m}\left(x_{i}\right) \neq y_{i}\right)=\sum_{i=1}^{N} w_{mi} I\left(G_{m}\left(x_{i}\right) \neq y_{i}\right)
    $$
    c. 计算 $G_m(x)$ 的系数
    $$
    \alpha_m = \frac{1}{2} \log(\frac{1-e_m}{e_m})
    $$
    这里的对数是自然对数

    d. 更新训练数据集的权值分布
    $$
    D_{m+1} = (w_{m+1,1}, …, w_{m+1, i}, …, w_{m+1, N}) \\
    w_{m+1, i} = \frac{w_{mi}}{Z_m} \exp(-\alpha_m I\left(G_{m}\left(x_{i}\right) = y_{i}\right)), \quad i =1,2,…,N
    $$
    这里, $Z_m$规范因子
    $$
    Z_m = \sum_{i = 1}^{N}w_{mi}\exp(-\alpha_m I\left(G_{m}\left(x_{i}\right) = y_{i}\right))
    $$
    $I\left(G_{m}\left(x_{i}\right) = y_{i}\right)$,当分类正确的时候取1,分类错误取0。二分类时可以 $y_iG_m(x_i)$ 代替

    它使 $D_{m+1}$ 称为一个概率分布

  3. 构建基本分类器的线性组合
    $$
    f(x) = \sum_{m=1}^{M}\alpha_mG_m(x)
    $$
    最终得到分类器
    $$
    G(x) = \text{sign}(f(x))
    $$

注: $e_m$ 大的,$\alpha_m$ 小,这个时候观察权值的更新公式,对于分子来说,正确率高的,分子小,正确率低的,分子大, 这样就达到了提高被前一轮弱分类器错误分类的样本的权值,降低分类正确的样本的权值的目的

Q:Adaboost终止条件是什么?为什么不容易过拟合?

有些代码将终止条件设置为一个超参数,即弱分类器的个数。

为什么不容易过拟合?

知乎: 俞扬的回答

Adaboost在实验中表现出更加不容易过拟合(相对于别的boosting方法)的现象,尤其是在实验中发现,迭代中经验误差都已经不变化了的情况下,测试误差居然还能下降一段时间。

对于二分类问题,AdaBoost的训练误差以指数速率下降就很具有吸引力

1.2 AdaBoost算法解释

AdaBoost = 加法模型 + 损失函数(指数函数) + 学习算法(前向分步算法)

  • 加法模型

$$
f(x) = \sum_{m=1}^{M} \beta_m b(x;\gamma_m)
$$

  • 损失函数


$$
L(y,f(x)) = \exp(-yf(x))\\
(\alpha_m, G_m(x)) = \arg \min_{\alpha,G}\sum_{i = 1}^{N}\exp[-y_i(f_{m-1}(x_i) +\alpha G(x_i))]
$$

1.3 Adaboost代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# 解决的问题:二分类,Y_label = {-1, 1} 这里简化特征X Xi = {0,1} (二值化处理)

# 算法实现思路
## 计算分类错误率,输入应该是:数据集和训练好的模弱分类器参数,返回预测结果和分类误差率
## 单层提升树: 输入:数据,权值分布 输出:创建的单层提升树
## 生成提升树:输入:数据, 输出:提升树
## 预测结果

def calc_e_Gx(X_trainArr, Y_trainArr, n, div, D):
'''
计算分类误差率
n,要操作的特征
div,划分的点
D, 权值分布
return 预测结果, 分类误差率
'''
# 初始化误差率
e = 0
# 单独提取X, Y
X_n = X_trainArr[:, n]
Y_n = Y_trainArr
predict = []

for i in range(len(X_n)):
if X_n[i] < div:
predict[i] = -1
if predict[i] != Y_train[i]: e += D[i]
else:
predict[i] = 1
if predict[i] != Y_train[i]: e += D[i]

return np.array(predict), e

def creatSingleTree(X_trainArr, Y_trainArr, D):
# 该函数可以用其他弱分类器代替
# 获得样本数目及特征数量
m, n = np.shape(X_trainArr)

# 单层树字典,用于存放当前提升树的参数,包括:分割点,预测结果,误差率(由预测结果计算),
# 该单层树所处理的特征
singleBoostTree = {}
# 初始化误差率,最大为100%
singleBoostTree['e'] = 1

# 遍历每一个特征,寻找用于划分的最合适的特征
for i in range(n):
# 由于特征进行了二值化处理,只能为0、1,因此切分点为 -0.5, 0.5,1.5
for div in [-0.5, 0.5, 1.5]:
Gx, e = calc_e_Gx(X_trainArr, Y_trainArr, i, div, D)
if e < singleBoostTree['e']:
singleBoostTree['e'] = e
singleBoostTree['div'] = div
singleBoostTree['Gx'] = Gx
singleBoostTree['feature'] = i
return singleBoostTree

def creatBoostingTree(X_trainArr, Y_trainArr, treeNum = 50):
'''
treeNum: 弱分类器的数目作为一个超参数,可以通过交叉验证挑选一个最好的
return: 提升树
'''
m, n = np.shape(X_trainArr)

# 初始化权值分布
D = np.array([1 / m] * m)
# 初始化树列表
iterationNum = 0
tree = []

for i in range(treeNum):
# 创建当层的提升树
iterationNum += 1
curTree = singleBoostTree(X_trainArr, Y_trainArr, D)
# 计算alpha
alpha = 1 / 2 * np.log((1 - curTree['e']) / curTree['e'])
Gx = curTree['Gx']
D = np.multiply(D, np.exp( -alpha * np.multiply(Y_trainArr, Gx))) / \
sum(np.multiply(D, np.exp( -alpha * np.multiply(Y_trainArr, Gx))))
curTree['alpha'] = alpha
tree.append(curTree)

# 当前训练集预测结果
finalpredict += alpha * Gx
# 当前预测误差数目
error_count = 0
for i in range(len(Y_trainArr)):
if np.sign(finalpredict)[i] != Y_trainArr[i]:
error_count += 1

error_rate = error_count / len(Y_trainArr)

# 如果误差已经为0了,那么就可以停止了不用再计算了
if error_rate == 0: return tree
print('Numbers of iteration: {}, \n error rate: {}'.format(iterationNum, error_rate))
return tree

def Gx_predict(x, div, feature):
if x[feature] < div:
return -1
else:
return 1

def model_predict(X_testArr, tree):
prediction = []
# 每一层的tree 有div,alpha,feature
for i in range(len(X_testArr)):
result = 0
for curTree in tree:
div = curTree['div']
alpha = curTree['alpha']
feature = curTree['feature']
result += alpha * Gx_predict(X_testArr[i], div, feature)
prediction.append(result)

return prediction

def model_score(X_testArr, Y_testArr, tree):
prediction = model_predict(X_testArr, tree)
error_count = 0
for i in range(len(Y_testArr)):
if prediction[i] != Y_testArr[i]:
error_count += 1
score = 1 - (error_count / len(Y_testArr))

return score

1.3 boosting tree

决策树为基函数的提升方法称为提升树。可以是二叉分类树,或二叉回归树。

  • 模型

$$
f_M(x) = \sum_{m = 1}^M T(x;\Theta_m)
$$

其中, $T(x;\Theta_m)$ 表示决策树, $\Theta_m$ 为决策树的参数, $M$ 为树的个数

  • 参数确定(经验风险极小化)

$$
\hat{\Theta_m}=\arg \min_{\Theta_m} \sum_{i=1}^{N} L\left(y_{i}, f_{m-1}\left(x_{i}\right)+T\left(x_{i}; \Theta_{m}\right)\right)
$$

  • 损失函数
    • 平方误差损失函数的回归问题
    • 指数损失函数的分类问题
    • 一般损失函数的决策问题

当分类问题是二分类,AdaBoost算法的弱分类器限制为二分类即可,就是boosting tree

  • 回归问题的提升树
    $$
    T(x;\Theta)=\sum_{j=1}^{J}c_j I(x \in R_j)
    $$
    其中,参数 $\Theta = \lbrace (R_1, c_1), (R_2, c_2),…,(R_J, c_J) \rbrace$ 表示树的区域划分和各区域上的常数, $J$ 是回归树的复杂度即叶结点的个数。

    • 采用平方误差损失函数
      $$
      \begin{align*}
      L(y,f_{m-1}(x) + T(x; \Theta_m)) &= [y - f_{m-1}(x) - T(x;\Theta_m)]^2\\
      &=[r-T(x;\Theta_m)]^2
      \end{align*}
      $$
      所以从损失函数可以看出,当前层树的确定,是通过拟合上一层模型的残差。所以算法可以通过将该层的训练数据给改为残差即可递归得到每一层的tree
  • 参数更新(梯度下降)

$$
-[\frac{\partial L(y, f(x_i))}{\partial f(x_i)}]{f(x)=f{m-1}(x)}
$$

2. Bagging[Beriman, 1996a]

采用自主抽样法(bootstrap sampling)

  • 确定样本集个数 $m$
  • 又放回的抽取 $m$ 个样本
  • 采样出 $T$ 个含 $m$ 样本的采样集
  • 然后基于每个样本集训练一个基学习器
  • 预测可以使用简单投票法

可以记录一下使用的数据,未被使用的数据可以用来对泛化性能进行“包外估计”

代码难度不大

使用random.sample()或者np.random.choice()就可以实现重抽样

random.sample()默认不重复抽样,可以抽取多维数据

np.random.choice() 默认重复抽样 replace=True, 不可以抽取多维数据,所以可以用这个函数产生下标。

3. Random Forest(RF)

Bagging 的变体,RF在基学习器构建Bagging集成的基础上,进一步在决策树的训练过程中引入了随机属性选择。具体来说, 传统决策树在选择划分属性时是在当前结点的属性集合(假定有d个属性)中选择一个最优属性; 而在RF中, 对基决策树的每个结点, 先从该结点的属性集合中随机选择一个包含k个属性的子集, 然后再从这个子集中选择一个最优属性用于划分。若令$k=d$,则基决策树的构建与传统决策树相同;若令$k = 1$,则是随机选择一个属性用于划分; 一般情况下, 推荐值$k = log_2d$

算法:

输入:训练数据 $T$ ,基决策树(CART),重抽样参数 $m$,属性集合参数 $k$

输出:最终分类器

for 1,2,…T

​ 重抽样数据(m个),记录未被抽取的数据

​ 随机抽取k个属性

​ 根据重抽样的数据和确定的属性,学习基分类器 (如:CART)

end for

输出:最终分类器

4. Stacking

  • 介绍

假设是五折的stacking,我们有一个train数据集和一个test数据集,那么一个基本的stacking框架会进行如下几个操作:

1、选择基模型。我们可以有xgboost,lightGBM,RandomForest,SVM,ANN,KNN,LR等等你能想到的各种基本算法模型。

2、把训练集分为不交叉的五份。我们标记为train1到train5。

3、从train1开始作为预测集,使用train2到train5建模(model1),然后预测train1,并保留结果;然后,以train2作为预测集,使用train1,train3到train5建模,预测train2,并保留结果(model2);如此进行下去…….直到把train1到train5各预测一遍;

4、把预测的结果按照train1到trian5的位置对应填补上,得到对train整个数据集在第一个基模型的一个stacking转换。

5、在上述建立的五个模型过程中,每个模型分别对test数据集进行预测,并最终保留这五列结果,然后对这五列取平均,作为第一个基模型对test数据的一个stacking转换。

6、然后进入第二层,将新特征作为第二层的train data,第一层的预测结果作为test data

7、一般使用LR(逻辑回归)作为第二层的模型进行建模预测。

但使用stacking会比较耗时,所以真正比赛的时候可以改为3折或4折

  • Stacking的输出层为什么用逻辑回归?

stacking的有效性主要来自于特征抽取。而表示学习中,如影随形的问题就是过拟合,试回想深度学习中的过拟合问题。

在[5]中,周志华教授也重申了stacking在使用中的过拟合问题。因为第二层的特征来自于对于第一层数据的学习,那么第二层数据中的特征中不该包括原始特征,以降低过拟合的风险。举例:

  • 第二层数据特征:仅包含学习到的特征
  • 第二层数据特征:包含学习到的特征 + 原始特征

另一个例子是,stacking中一般都用交叉验证来避免过拟合,足可见这个问题的严重性。

为了降低过拟合的问题,第二层分类器应该是较为简单的分类器,广义线性如逻辑回归是一个不错的选择。在特征提取的过程中,我们已经使用了复杂的非线性变换,因此在输出层不需要复杂的分类器。这一点可以对比神经网络的激活函数或者输出层,都是很简单的函数,一点原因就是不需要复杂函数并能控制复杂度。

因此,stacking的输出层不需要过分复杂的函数,用逻辑回归还有额外的好处:

  • 配合L1正则化还可以进一步防止过拟合
  • 配合L1正则化还可以选择有效特征,从第一层的学习器中删除不必要的分类器,节省运算开销。
  • 逻辑回归的输出结果还可被理解为概率

Reference

[1]. https://github.com/Dod-o/

[2]. 《统计学习方法》 – 李航

[3]. 《机器学习》 – 周志华

[4]. 【SPA大赛】腾讯广告点击大赛:对stacking的一些基本介绍

[5]. Zhou, Z.H., 2012. Ensemble methods: foundations and algorithms. CRC press.

基本的分类与回归方法。本文主要是关于分类的决策树。

通常包括三个步骤:特征选择、决策树的生成和决策树的修剪

1. 模型与学习

  • 模型

可以将决策模型看作是一个if-then结构,具有的重要性质是互斥且完备

  • 学习

目标是根据给定的训练数据集构建一个决策树模型, 使它能够对实例进行正确的分类。通常使用损失函数(正则化的极大似然函数)表示这一目标。

决策树学习的算法通常是一个递归地选择最优特征, 并根据该特征对训练数据进行分割, 使得对各个子数据集有一个最好的分类的过程。

如果特征数量很多, 也可以在决策树学习开始的时候,对特征进行选择,只留下对训练数据有足够分类能力的特征

一般而言决策树学习算法包含

  • 特征选择
  • 决策树的生成
  • 决策树的剪枝过程

常用的算法有ID3、C4.5、CART

2. 特征选择

特征选择在于选取对训练数据具有分类能力的特征。 如果利用一个特征进行分类的结果与随机分类的结果没有很大差别, 则称这个特征是没有分类能力的

通常选择的准则是信息增益信息增益比

2.1 信息增益

随机变量X的熵定义为
$$
H(X) = H(p) = -\sum_{i=1}^{n} p_{i} \log p_{i}
$$
熵越大,随机变量的不确定性就越大。

  • 条件熵

已知随机变量X的条件下随机变量Y的不确定性
$$
H(Y \mid X)=\sum_{i=1}^{n} p_{i} H(Y \mid X=x_{i})
$$
其中 $p_i = P(X=x_i)$ .

当熵和条件熵中的概率由数据估计(特别是极大似然估计)得到时, 所对应的熵与条件熵分别称为经验熵(empirical entropy)和经验条件熵。

  • 信息增益

表示得知特征 $X$ 的信息而使得类 $Y$ 的信息的不确定性减少的程度。

特征A对训练数据集D的信息增益 $g(D,A)$,定义为集合 $D$ 的经验熵 $H(D)$ 与特征 $A$ 给定条件下 $D$ 的经验条件熵 $H(D|A)$之差
$$
g(D,A) = H(D) - H(D|A)
$$
一般地, 熵H(y)与条件熵H(y|X)之差称为互信息(mutual information)。 决策树学习中的信息增益等价于训练数据集中类与特征的互信息

根据信息增益准则的特征选择方法是: 对训练数据集(或子集)D, 计算其每个特
征的信息增益,并比较它们的大小,选择信息增益最大的特征。

设训练数据集为 $D,|D|$ 表示其样本容量,即样本个数。设有 $K$ 个类 $C_{k}, k=$ $1,2, \cdots, K,|C_{k}|$ 为属于类 $C_{k}$ 的样本个数, $\sum_{k=1}^{K}\left|C_{k}\right|=|D|$ 设特征 $A$ 有 $n$ 个不同的取值 $\lbrace a_{1}, a_{2}, \cdots, a_{n} \rbrace$ 根据特征 $A$ 的取值将 $D$ 划分为 $n$ 个子集 $D_{1}, D_{2}, \cdots, D_{n}$ ,$|D_{i}|$ 为 $D_{i}$ 的样本个数, $\sum_{i=1}^{n}\left|D_{i}\right|=|D|$ 记子集 $D_{i}$ 中属于类 $C_{k}$ 的样本的集合为 $D_{i k}$ ,即 $D_{ik}=D_{i} \cap C_{k},|D_{ik}|$ 为 $D_{ik}$ 的样本个数。于是信息增益的算法如下。

算法5.1(信息增益的算法)
输入: 训练数据集 $D$ 和特征 $A$;

输出:特征 $A$ 对训练数据集 $D$ 的信息增益 $g(D, A)$ 。
(1) 计算数据集 $D$ 的经验熵 $H(D)$
$$
H(D)=-\sum_{k=1}^{K} \frac{\left|C_{k}\right|}{|D|} \log_{2} \frac{|C_{k}|}{|D|}
$$
(2) 计算特征 $A$ 对数据集 $D$ 的经验条件嫡 $H(D \mid A)$
$$
H(D \mid A)=\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|} H\left(D_{i}\right)=-\sum_{i=1}^{n} \frac{\left|D_{i}\right|}{|D|} \sum_{k=1}^{K} \frac{\left|D_{i k}\right|}{\left|D_{i}\right|} \log_{2} \frac{\left|D_{ik}\right|}{\left|D_{i}\right|}
$$
(3) 计算信息增益
$$
g(D, A)=H(D)-H(D \mid A)
$$

2.2 信息增益比

  • 定义(信息增益与熵之比)
    $$
    g_{R}(D, A)=\frac{g(D, A)}{H_{A}(D)}
    $$

其中,$H_{A}(D)=-\sum_{i=1}^{n} \frac{|D_{i}|}{|D|} \log {2} \frac{|D{i}|}{|D|}, n$ 是特征 $A$ 取值的个数。

3. 决策树的生成

3.1 ID3算法

具体方法是: 从根结点(root node)开始, 对结点计算所有可能的特征的信息增益, 选择信息增益最大的特征作为结点的特征, 由该特征的不同取值建立子结点;再对子结点递归地调用以上方法, 构建决策树; 直到所有特征的信息增益均很小或没有特征可以选择为止。最后得到一颗决策树。ID3算法相当于用极大似然法进行概率模型的选择。

算法3.1 (ID3算法)

输入:训练数据集 $D$ ,特征集 $A$ 阈值 $\varepsilon$

输出:决策树 $T$ 。

(1)若 $D$ 中所有实例属于同一类 $C_{k},$ 则 $T$ 为单结点树,并将类 $C_{k}$ 作为该结点的类标记,返回 $T$;

(2)若 $A=\varnothing$ ,则 $T$ 为,sh单结点树,并将 $D$ 中实例数最大的类 $C_{k}$ 作为该结点的类标记,返回 $T$;

(3)否则,计算 $A$ 中各特征对 $D$ 的fanjia信息增益,选择信息增益最大的特征 $A_{g}$ ;

(4)如果 $A_{g}$ 的信息增益小于阈值 $\varepsilon$ ,则设置 $T$ 为单结点树,并将 $D$ 中实例数最大的类 $C_{k}$ 作为该结点的类标记,返回 $T$;

(5)否则,对 $A_{g}$ 的每一可能值 $a_{i},$ 依 $A_{g}=a_{i}$ 将 $D$ 分割为若干非空子集 $D_{i},$ 将$D_{i}$ 中实例数最大的类作为标记,构建子结点,由结点及其子结点构成树 $T$ ,返回 $T$;

(6)对第 $i$ 个子结点,以 $D_{i}$ 为训练集,以 $A-\lbrace A_{g}\rbrace$ 为特征集,递归地调用步 (1)$\sim$ 步 $(5),$ 得到子树 $T_{i},$ 返回 $T_{i}$

缺点:只有树生成,没有树剪枝,容易过拟合

3.2 C4.5算法

与ID3相比,在生成过程中用信息增益比来选择特征。

算法 3.2 (C4.5的生成算法)

输入: 训练数据集 $D$, 特征集 $A$ 阈值 $\varepsilon ;$

输出: 决策树 $T_{\circ}$

(1)如果 $D$ 中所有实例属于同一类 $C_{k},$ 则置 $T$ 为单结点树,并将 $C_{k}$ 作为该结点的类,返回 $T$;

(2)如果 $A=\varnothing,$ 则置 $T$ 为单结点树,并将 $D$ 中实例数最大的类 $C_{k}$ 作为该结的类,返回 $T$;

(3)否则,计算 $A$ 中各特征对 $D$ 的信息增益比,选择信息增益比最大的特征 $A_{g} ;$

(4)如果 $A_{g}$ 的信息增益比小于阈值 $\varepsilon$ ,则置 $T$ 为单结点树,并将 $D$ 中实例数最大的类 $C_{k}$ 作为该结点的类,返回 $T$;

(5)否则,对 $A_{g}$ 的每一可能值 $a_{i},$ 依 $A_{g}=a_{i}$ 将 $D$ 分割为子集若干非空 $D_{i}$ ,将 $D_{i}$ 中实例数最大的类作为标记,构建子结点,由结点及其子结点构成树 $T$ ,返回 $T$;

(6)对结点 $i$,以 $D_{i}$ 为训练集,以 $A-\left{A_{g}\right}$ 为特征集,递归地调用步(1)$\sim$步 $(5),$ 得到子树 $T_{i},$ 返回 $T_{i}$ 。

4. 决策树的剪枝

以上两种算法均未考虑树的复杂度,这样产生的决策树容易出现过拟合的问题。因此需要将生成的树进行简化。

决策树的剪枝往往通过极小化决策树整体的损失函数或代价函数来实现。设树 $T$ 的叶节点个数为 $|T|$ ,$t$ 是树 $T$ 的叶节点,该叶节点有 $N_t$ 个样本点,其中 $k$ 类样本点有 $N_{tk}$ 个, $H_t(T)$ 为叶节点 $t$ 上的经验熵。 $\alpha$ 为参数。则决策树的损失函数可以定义为
$$
\begin{array}{c}
C_{\alpha}(T)=\sum_{t=1}^{|T|} N_{t} H_{t}(T)+\alpha|T| \\
H_{t}(T)=-\sum_{k} \frac{N_{tk}}{N_{t}} \log \frac{N_{tk}}{N_{t}}
\end{array}
$$
将第一项记做 $C(T)$,这时有
$$
C_{\alpha}(T) = C(T)+ \alpha|T|
$$
第一项表示对训练数据的预测误差,第二项表示模型的复杂度。$\alpha \geq 0$ 控制两者之间的影响。由此可以看出,决策树的生成学习局部的模型,而决策树剪枝学习整体的模型。

算法 4.1 (树的剪枝算法)

输入:生成算法产生的整个树 $T$ ,参数 $\alpha$

输出:修剪后的子树 $T_{\alpha}$

(1)计算每个结点的经验熵

(2)递归的从树的叶结点向上回缩。

设一组叶结点回缩到其父结点之前与之后的整体树分别为 $T_B$ 与 $T_A$ ,其对应的损失函数值分别是 $C_{\alpha}(T_B)$ 与 $C_{\alpha}(T_A)$ , 如果
$$
C_{\alpha}(T_A) \leq C_{\alpha}(T_B)
$$
则进行剪枝,即将父结点变为新的叶结点。

(3)返回2,直至不能继续为止,得到损失函数最小的子树 $T$

5. CART (classification and regresstion tree)算法 –1984

CART特征选择、树的生成及剪枝组成的二叉树。既可以用于分类也可以用于回归。左分支是“是”的分支,右分支是“否”的分支。

CART是在给定输入随机变量 $X$ 条件下输出随机变量 $Y$ 的条件概率分布的学习方法。

5.1 CART的生成

回归树:平方误差最小化准则

分类树:基尼指数(Gini index) 最小化准则

  1. 回归树的生成

每个输入空间 $R_m$ 的标签值为,所有输入空间中实例的标签的均值
$$
\hat{c} = ave(y_i|x_i \in R_m)
$$

算法 5.1 (最小二乘回归树生成算法)
输入: 训练数据集 $D$;

输出: 回归树 $f(x)$

在训练数据集所在的输入空间中,递归地将每个区域划分为两个子区域并决定每个子区域上的输出值,构建二叉决策树:

(1)选择最优切分变量 $j$ 与切分点 $s$ ,求解
$$
\min_{j, s}\left[\min_{c_{1}} \sum_{x_{i} \in R_{1}(j, s)}\left(y_{i}-c_{1}\right)^{2}+\min_{c_{2}} \sum_{x_{i} \in R_{2}(j, s)}\left(y_{i}-c_{2}\right)^{2}\right]
$$
遍历变量 $j,$ 对固定的切分变量 $j$ 扫描切分点 $s$ ,选择使上述目标函数达到最小值的对
$(j, s)$

(2)用选定的对 $(j, s)$ 划分区域并决定相应的输出值:
$$
\begin{array}{c}
R_{1}(j, s)=\lbrace x \mid x^{(j)} \leqslant s\rbrace, \quad R_{2}(j, s)=\lbrace x \mid x^{(j)}>s\rbrace \\
\hat{c}{m}=\frac{1}{N{m}} \sum_{x_{i} \in R_{m}(j, s)} y_{i}, \quad x \in R_{m}, \quad m=1,2
\end{array}
$$
(3)继续对两个子区域调用步骤 $(1),(2),$ 直至满足停止条件。

(4)将输入空间划分为 $M$ 个区域 $R_{1}, R_{2}, \cdots, R_{M}$ ,生成决策树:
$$
f(x)=\sum_{m=1}^{M} \hat{c}{m} I\left(x \in R{m}\right)
$$
当数据量大的时候,遍历的效率不高。

  1. 分类树的生成

分类树用基尼指数选择最优特征, 同时决定该特征的最优二值切分点。

基尼指数的定义
$$
\text{Gini}(p) = \sum_{k = 1}^{K} p_k (1 - p_k) = 1 - \sum_{k=1}^{K}p_k^2
$$
对于给定样本集合D, $p_k = \frac{|C_K|}{|D|}$

基尼指数表示集合D的不确定性,数值越大,样本集合的不确定性就越大。这一点与熵类似。

算法 $5.6(\mathrm{CART}$ 生成算法)

输入: 训练数据集 $D$ ,停止计算的条件;

输出: CART 决策树。

根据训练数据集,从根结点开始,递归地对每个结点进行以下操作,构建二叉决策树:

(1)设结点的训练数据集为 $D,$ 计算现有特征对该数据集的基尼指数。此时,对每一个特征 $A$ ,对其可能取的每个值 $a$,根据样本点对 $A=a$ 的测试为“是”或“否” 将 $D$ 分割成 $D_{1}$ 和 $D_{2}$ 两部分, 计算 $A=a$ 时的基尼指数。

(2)在所有可能的特征 $A$ 以及它们所有可能的切分点 $a$ 中,选择基尼指数最小的特征及其对应的切分点作为最优特征与最优切分点。依最优特征与最优切分点,从现结点生成两个子结点,将训练数据集依特征分配到两个子结点中去。

(3)对两个子结点递归地调用 $(1),(2),$ 直至满足停止条件。

(4) 生成 CART 决策树。

5.2 CART剪枝

具体地, 从整体树 $T_0$ 开始剪枝。 对 $T_0$ 的任意内部结点 $t$ ,以 $t$ 为单结点树的损失函数是
$$
C_{\alpha}(t) = C(t) + \alpha
$$
以 $t$ 为根节点的子树 $T_t$ 的损失函数是
$$
C_{\alpha}(T_t) = C(T_t) + \alpha |T_t|
$$
当 $\alpha = 0$ 及 $\alpha$ 充分小时,有
$$
C_{\alpha}(T_t) < C_{\alpha}(t)
$$
当 $\alpha$ 增大时,在某一 $\alpha$ 有
$$
C_{\alpha}(T_t) = C_{\alpha}(t)
$$
所以只要 $\alpha =\frac{C(t)-C\left(T_{t}\right)}{\left|T_{t}\right|-1}$,就可以选择结点更少的t。

而Beriman等人证明:可以用递归的方法对树进行剪枝。将 $\alpha$ 逐渐增大,$0 = \alpha_0 < \alpha_1 <\cdots <\alpha_n < +\infty$ ,产生一系列的区间 $[\alpha_i, \alpha_{i+1}), i = 0,1,…,n$ ;剪枝得到的子树序列对应着区间 $\alpha \in [\alpha_i, \alpha_{i+1})$,的最优子树序列 $\lbrace T_0, T_1, \cdots, T_n \rbrace$。然后利用交叉验证挑出一个最好的

算法 5.7 ($\mathbf{C A R T}$ 剪枝算法)

输入: $\mathrm{CART}$ 算法生成的决策树 $T_{0} ;$

输出:最优决策树 $T_{\alpha^{\circ}}$

(1)设 $k=0, T=T_{0}$ 。

(2)设 $\alpha=+\infty$ 。

(3)自下而上地对各内部结点 $t$ 计算 $C\left(T_{t}\right),\left|T_{t}\right|$ 以及
$$
\begin{aligned}
g(t) &=\frac{C(t)-C\left(T_{t}\right)}{\left|T_{t}\right|-1} \\
\alpha &=\min (\alpha, g(t))
\end{aligned}
$$
这里, $T_{t}$ 表示以 $t$ 为根结点的子树, $C\left(T_{t}\right)$ 是对训练数据的预测误差, $\left|T_{t}\right|$ 是 $T_{t}$ 的叶 结点个数。

(4)对 $g(t)=\alpha$ 的内部结点 $t$ 进行剪枝,并对叶结点 $t$ 以多数表决法决定其类,
得到树 $T$ 。

(5)设 $k=k+1, \alpha_{k}=\alpha, T_{k}=T_{\circ}$

(6)如果 $T_{k}$ 不是由根结点及两个叶结点构成的树,则回到步骤 (2)$;$ 否则令
$T_{k}=T_{n}$

(7)采用交叉验证法在子树序列 $T_{0}, T_{1}, \cdots, T_{n}$ 中选取最优子树 $T_{\alpha} \circ$

6. 代码实现

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import Counter

import math
from math import log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 经验熵
def entropy(datasets):
data_length = len(datasets)
label_count = {}
for i in range(data_length):
label = datasets[i][-1]
if label not in label_count:
label_count[label] = 0
label_count[label] += 1
entropy = - sum([(p / data_length) * log(p / data_length, 2)
for p in label_count.values()])
return entropy

# 条件经验熵
def cond_entropy(datasets, axis=0):
"""
求数据集datasets中第axis列的条件经验熵
"""
data_length = len(datasets)
feature_sets = {}
for i in range(data_length):
feature = datasets[i][axis]
if feature not in feature_sets:
feature_sets[feature] = []
feature_sets[feature].append(datasets[i])
cond_entropy = sum([(len(p) / data_length) * entropy(p)
for p in feature_sets.values()])
return cond_entropy

# 信息增益 = 经验熵 - 条件经验熵
def info_gain(entropy, cond_entropy):
return entropy - cond_entropy

# 利用信息增益选择根节点
def info_gain_train(datasets):
data_dim = len(datasets[0]) - 1
ent = entropy(datasets)
info_gain_feature = []
for i in range(data_dim):
i_info_gain = info_gain(ent, cond_entropy(datasets, axis=i))
info_gain_feature.append(i_info_gain)
print('特征{}的信息增益为:{}'.format(i + 1, i_info_gain))
best_feature = max(info_gain_feature)
return '特征{}的信息增益最大,选择根节点特征'.format(info_gain_feature.index(best_feature) + 1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
datasets = [['青年', '否', '否', '一般', '否'],
['青年', '否', '否', '好', '否'],
['青年', '是', '否', '好', '是'],
['青年', '是', '是', '一般', '是'],
['青年', '否', '否', '一般', '否'],
['中年', '否', '否', '一般', '否'],
['中年', '否', '否', '好', '否'],
['中年', '是', '是', '好', '是'],
['中年', '否', '是', '非常好', '是'],
['中年', '否', '是', '非常好', '是'],
['老年', '否', '是', '非常好', '是'],
['老年', '否', '是', '好', '是'],
['老年', '是', '否', '好', '是'],
['老年', '是', '否', '非常好', '是'],
['老年', '否', '否', '一般', '否'],
]
labels = [u'年龄', u'有工作', u'有自己的房子', u'信贷情况', u'类别']
train_data = pd.DataFrame(datasets, columns=labels)
1
info_gain_train(np.array(datasets))

ID3算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# 定义节点类 二叉树
class Node:
def __init__(self, root=True, label=None, feature_name=None, feature=None):
self.root = root
self.label = label
self.feature_name = feature_name
self.feature = feature
self.tree = {}
self.result = {
'label:': self.label,
'feature_name': self.feature_name,
'tree': self.tree
}

def __repr__(self):
return '{}'.format(self.result)

def add_node(self, val, node):
self.tree[val] = node

def predict(self, features):
if self.root is True:
return self.label
return self.tree[features[self.feature]].predict(features)


class DTree:
def __init__(self, epsilon=0.1):
self.epsilon = epsilon
self._tree = {}

# 熵
@staticmethod
def calc_ent(datasets):
data_length = len(datasets)
label_count = {}
for i in range(data_length):
label = datasets[i][-1]
if label not in label_count:
label_count[label] = 0
label_count[label] += 1
ent = -sum([(p / data_length) * log(p / data_length, 2)
for p in label_count.values()])
return ent

# 经验条件熵
def cond_ent(self, datasets, axis=0):
data_length = len(datasets)
feature_sets = {}
for i in range(data_length):
feature = datasets[i][axis]
if feature not in feature_sets:
feature_sets[feature] = []
feature_sets[feature].append(datasets[i])
cond_ent = sum([(len(p) / data_length) * self.calc_ent(p)
for p in feature_sets.values()])
return cond_ent

# 信息增益
@staticmethod
def info_gain(ent, cond_ent):
return ent - cond_ent

def info_gain_train(self, datasets):
count = len(datasets[0]) - 1
ent = self.calc_ent(datasets)
best_feature = []
for c in range(count):
c_info_gain = self.info_gain(ent, self.cond_ent(datasets, axis=c))
best_feature.append((c, c_info_gain))
# 比较大小
best_ = max(best_feature, key=lambda x: x[-1])
return best_

def train(self, train_data):
"""
input:数据集D(DataFrame格式),特征集A,阈值eta
output:决策树T
"""
_, y_train, features = train_data.iloc[:, :
-1], train_data.iloc[:,
-1], train_data.columns[:
-1]
# 1,若D中实例属于同一类Ck,则T为单节点树,并将类Ck作为结点的类标记,返回T
if len(y_train.value_counts()) == 1:
return Node(root=True, label=y_train.iloc[0])

# 2, 若A为空,则T为单节点树,将D中实例树最大的类Ck作为该节点的类标记,返回T
if len(features) == 0:
return Node(
root=True,
label=y_train.value_counts().sort_values(
ascending=False).index[0])

# 3,计算最大信息增益 同5.1,Ag为信息增益最大的特征
max_feature, max_info_gain = self.info_gain_train(np.array(train_data))
max_feature_name = features[max_feature]

# 4,Ag的信息增益小于阈值eta,则置T为单节点树,并将D中是实例数最大的类Ck作为该节点的类标记,返回T
if max_info_gain < self.epsilon:
return Node(
root=True,
label=y_train.value_counts().sort_values(
ascending=False).index[0])

# 5,构建Ag子集
node_tree = Node(
root=False, feature_name=max_feature_name, feature=max_feature)

feature_list = train_data[max_feature_name].value_counts().index
for f in feature_list:
sub_train_df = train_data.loc[train_data[max_feature_name] ==
f].drop([max_feature_name], axis=1)

# 6, 递归生成树
sub_tree = self.train(sub_train_df)
node_tree.add_node(f, sub_tree)

# pprint.pprint(node_tree.tree)
return node_tree

def fit(self, train_data):
self._tree = self.train(train_data)
return self._tree

def predict(self, X_test):
return self._tree.predict(X_test)
1
2
3
4
datasets, labels = create_data()
data_df = pd.DataFrame(datasets, columns=labels)
dt = DTree()
tree = dt.fit(data_df)
1
2
tree
# {'label:': None, 'feature_name': '有自己的房子', 'tree': {'否': {'label:': None, 'feature_name': '有工作', 'tree': {'否': {'label:': '否', 'feature_name': None, 'tree': {}}, '是': {'label:': '是', 'feature_name': None, 'tree': {}}}}, '是': {'label:': '是', 'feature_name': None, 'tree': {}}}}
1
2
dt.predict(['老年', '否', '否', '一般'])
# '否'

scikit-learn实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## sklearn里并没有实现后剪枝,只有预剪枝操作,例如设置树的最大深度max_depth

iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['label'] = iris.target
df.columns = [
'sepal length', 'sepal width', 'petal lenght', 'petal width', 'label'
]
def shuffle(X,Y):
randomize = np.arange(len(X))
np.random.shuffle(randomize)
return X[randomize], Y[randomize]

data = np.array(df)
X, Y = data[:, :2], data[:,-1]
X, y = shuffle(X,Y)
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size = 0.3)
1
2
3
4
from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier()
clf.fit(X_train, Y_train)
clf.score(X_test,Y_test)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from sklearn.tree import DecisionTreeClassifier
from sklearn import preprocessing
import numpy as np
import pandas as pd

from sklearn import tree
import graphviz

features = ["年龄", "有工作", "有自己的房子", "信贷情况"]
X_train = pd.DataFrame([
["青年", "否", "否", "一般"],
["青年", "否", "否", "好"],
["青年", "是", "否", "好"],
["青年", "是", "是", "一般"],
["青年", "否", "否", "一般"],
["中年", "否", "否", "一般"],
["中年", "否", "否", "好"],
["中年", "是", "是", "好"],
["中年", "否", "是", "非常好"],
["中年", "否", "是", "非常好"],
["老年", "否", "是", "非常好"],
["老年", "否", "是", "好"],
["老年", "是", "否", "好"],
["老年", "是", "否", "非常好"],
["老年", "否", "否", "一般"]
])
y_train = pd.DataFrame(["否", "否", "是", "是", "否",
"否", "否", "是", "是", "是",
"是", "是", "是", "是", "否"])

#数据预处理
le_x = preprocessing.LabelEncoder()
le_x.fit(np.unique(X_train)) # 找出X_tran中的特征值有哪些
# array(['一般', '中年', '否', '好', '是', '老年', '青年', '非常好'], dtype=object)
# 这样 0代表‘一般’,1代表‘中年’,....
X_train = X_train.apply(le_x.transform) # apply()可以传入一个函数
le_y = preprocessing.LabelEncoder()
le_y.fit(np.unique(y_train))
y_train = y_train.apply(le_y.transform)

# 调用sklearn.DT训练模型
model_tree = DecisionTreeClassifier()
model_tree.fit(X_train, y_train)

Reference

[1]. https://github.com/fengdu78/lihang-code/

[2]. 《统计学习与方法》 – 李航

生成模型

对于给定数据集,首先基于特征条件独立假设学习输入输出的联合概率分布; 然后基于此模型, 对给定的输入 $x$, 利用贝叶斯定理求出后验概率最大的输出 $y$。 朴素贝叶斯法实现简单, 学习与预测的效率都很高, 是一种常用的方法。

1. 学习与分类

  • 首先学习先验概率分布以及条件概率分布。

$$
P(Y=c_k),\quad k=1,2,…k
$$

$$
P(X=x \mid Y=c_{k})=P(X^{(1)}=x^{(1)}, \cdots, X^{(n)}=x^{(n)} \mid Y=c_{k}), \quad k=1,2, \cdots, K
$$

于是学到联合概率分布 $P(X,Y)$

由于朴素贝叶斯假设条件独立,于是条件概率为
$$
\begin{aligned}
P(X=x \mid Y=c_{k}) &=P(X^{(1)}=x^{(1)}, \cdots, X^{(n)}=x^{(n)} \mid Y=c_{k}) \\
&=\prod_{j=1}^{n} P(X^{(j)}=x^{(j)} \mid Y=c_{k})
\end{aligned}
$$

  • 参数学习(极大似然)
    $$
    P\left(Y=c_{k}\right)=\frac{\sum_{i=1}^{N} I\left(y_{i}=c_{k}\right)}{N}, \quad k=1,2, \cdots, K
    $$

    $$
    \begin{array}{l}
    P\left(X^{(j)}=a_{j l} \mid Y=c_{k}\right)=\frac{\sum_{i=1}^{N} I\left(x_{i}^{(j)}=a_{j l}, y_{i}=c_{k}\right)}{\sum_{i=1}^{N} I\left(y_{i}=c_{k}\right)} \\
    j=1,2, \cdots, n ; \quad l=1,2, \cdots, S_{j} ; \quad k=1,2, \cdots, K
    \end{array}
    $$

  • 分类器

$$
y=f(x)=\arg \max_{c_{k}} \frac{P\left(Y=c_{k}\right) \prod_{j} P\left(X^{(j)}=x^{(j)} \mid Y=c_{k}\right)}{\sum_{k} P\left(Y=c_{k}\right) \prod_{j} P\left(X^{(j)}=x^{(j)} \mid Y=c_{k}\right)}
$$

2. 原理(后验概率最大化 == 期望风险最小化)

假设选择0-1损失函数
$$
L(Y,f(X)) = \begin{cases}
1, & Y \neq f(X) \\
0, & Y = f(X)
\end{cases}
$$
期望风险函数为
$$
R_{\exp }(f)=E[L(Y, f(X))]
$$
期望是对联合分布 $P(X,Y)$取的,由此条件期望
$$
R_{\exp }(f)=E_{X} \sum_{k=1}^{K}\left[L\left(c_{k}, f(X)\right)\right] P\left(c_{k} \mid X\right)
$$
为了使期望风险最小化,只需对 $X=x$ 逐个极小化,
$$
\begin{aligned}
f(x) &=\arg \min_{y \in \mathcal{Y}} \sum_{k=1}^{K} L\left(c_{k}, y\right) P\left(c_{k} \mid X=x\right) \\
&=\arg \min_{y \in \mathcal{Y}} \sum_{k=1}^{K} P\left(y \neq c_{k} \mid X=x\right) \\
&=\arg \min_{y \in \mathcal{Y}}\left(1-P\left(y=c_{k} \mid X=x\right)\right) \\
&=\arg \max_{y \in \mathcal{Y}} P\left(y=c_{k} \mid X=x\right)
\end{aligned}
$$

3. 贝叶斯估计(拉普拉斯平滑)

用极大似然估计可能会出现所要估计的概率值为0的情况。 这时会影响到后验概率的计算结果, 使分类产生偏差。 解决这一问题的方法是采用贝叶斯估计。具体的条件概率的贝叶斯估计是
$$
P_{\lambda}\left(X^{(j)}=a_{j l} \mid Y=c_{k}\right)=\frac{\sum_{i=1}^{N} I\left(x_{i}^{(j)}=a_{j l}, y_{i}=c_{k}\right)+\lambda}{\sum_{i=1}^{N} I\left(y_{i}=c_{k}\right)+S_{j} \lambda}
$$
$l=1,2,\cdots,S_j,k=1,2,…,K$ ,$\lambda \geq 0$,等价于在随机变量各个取值上赋予一个正数。$\lambda = 0$ 是极大似然估计。常取 $\lambda = 1$,称为拉普拉斯平滑。检验可知,概率和为1,并且每个概率大于0

先验概率的贝叶斯估计是
$$
P_{\lambda}\left(Y=c_{k}\right)=\frac{\sum_{i=1}^{N} I\left(y_{i}=c_{k}\right)+\lambda}{N+K \lambda}
$$

4. 代码实现

1
2
3
4
5
6
7
8
9
10
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

from collections import Counter
import math
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# import data
iris = load_iris()
df = pd.DataFrame(iris.data, columns=iris.feature_names)
df['labels'] = iris.target
df.columns = [
'sepal length', 'sepal width', 'petal length', 'petal width', 'label'
]
data = np.array(df)
X, Y = data[:, :-1], data[:, -1]

def _shuffle(X, Y):
randomize = np.arange(len(X))
np.random.shuffle(randomize)
return X[randomize], Y[randomize]

X, Y = _shuffle(X,Y)

X_train, X_test, Y_train, Y_test = train_test_split(X,Y, test_size = 0.3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class NaiveBayes:
def __init__(self):
self.model = None
self.Y_mean = None


# 计算均值
def mean(self, X):
return sum(X) / float(len(X))

# 计算标准差
def std(self, X):
avg = self.mean(X)
return math.sqrt(sum([pow(x - avg, 2) for x in X]) / float(len(X)))

# 概率密度函数
def gaussian_probability(self, x, mean, std):
exp = math.exp(-(math.pow(x - mean, 2) / (2 * math.pow(std, 2))))
return (1 / (math.sqrt(2 * math.pi)) * std) * exp

# 计算X_train的mean 和std
def summarize(self, X_train): # *X_train 星号作用是解包然后逐个传入
summaries = [(self.mean(i), self.std(i)) for i in zip(*X_train)] # 所以zip(*X_train) == zip([1,2,3],[2,3,4],...)
return summaries

def fit(self, X, Y):
labels = list(set(Y))
data = {label: [] for label in labels} # 初始化
self.Y_mean = np.zeros(len(labels))
for x, label in zip(X, Y):
data[label].append(x)
self.Y_mean[int(label)] += 1.
self.Y_mean /= len(Y)
# 计算P(x_i|y_k) for i in range...
self.model = {
label:self.summarize(value)
for label, value in data.items()
}
return 'gaussianNB train done'

def calculate_probabilities(self, input_data):
# summaries: {0: [(mean1,std1),(mean2,std2),(mean3,std3),(mean4,std4)], 1:...}
probabilities = {}
for label, value in self.model.items(): # value --> summaries
probabilities[label] = self.Y_mean[int(label)] # P(C_i)
for i in range(len(value)):
mean, std = value[i]
probabilities[label] *= self.gaussian_probability(input_data[i], mean, std)
# 计算P(x|c_i) * P(C_i)的概率
return probabilities

def predict(self, X_test):
# 这里X_test为单个实例
result = []
for i in range(len(X_test)):
label = sorted(self.calculate_probabilities(X_test[i]).items(), key=lambda x:x[-1])[-1][0] # sorted 默认从小到大 返回一个list
# 如[(1, 75), (0, 85), (2, 95)]
result.append(label)

return result

def score(self, X_test, Y_test):
right = 0
predictions = self.predict(X_test)
for i in range(len(Y_test)):
if predictions[i] == Y_test[i]:
right += 1
return right / float(len(Y_test))
1
model = NaiveBayes()
1
2
model.fit(X_train, Y_train)
# 'gaussianNB train done'
1
result1 = model.predict(X_test)
1
model.score(X_test, Y_test)

scikit-learn 实例

1
2
3
4
5
6
7
from sklearn.naive_bayes import GaussianNB
clf = GaussianNB()
#X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
#Y = np.array([1, 1, 1, 2, 2, 2])
clf.fit(X_train, Y_train)
result2 = clf.predict(X_test)
clf.score(X_test, Y_test)

Reference:

[1]. https://github.com/fengdu78/lihang-code

[2]. 《统计学习方法》 – 李航

基本的分类和回归方法

给定一个训练数据集, 对新的输入实例, 在训练数据集中找到与该实例最邻近的k个实例, 这k个实例的多数属于某个类, 就把该输入实例分为这个类。

1. k 近邻模型

三个基本要素:距离度量,k值选择、分类决策规则

1.1 距离度量

$x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(n)}\right)^{\mathrm{T}}$ $L_{p}$ 距离:
$$
L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{l=1}^{n}\left|x_{i}^{(l)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}}
$$
当 $p=2$ 为欧式距离(常用)

当 $p=1 $ 为曼哈顿距离

当 $p=\infty$ 时,是各个坐标距离的最大值
$$
L_{\infty}(x_{i}, x_{j})=\max_{l}\left|x_{i}^{(l)}-x_{j}^{(l)}\right|
$$

1.2 k值选择

k值选择较小时,近似误差小,估计误差大,过拟合

k值选择较大时,近似误差大,可以减少估计误差

Trick: k值一般取一个比较小的数值。 通常采用交叉验证法来选取最优的k值

1.3 分类决策规则

往往是多数表决:由输入实例的k个邻近的训练实例中的多数类决定输入实例的类

误分类率:
$$
\frac{1}{k} \sum_{x_{i} \in N_{k}(x)} I\left(y_{i} \neq c_{j}\right)=1-\frac{1}{k} \sum_{x_{i} \in N_{k}(x)} I\left(y_{i}=c_{j}\right)
$$

2. 模型实现:kd树

实现k近邻法时, 主要考虑的问题是如何对训练数据进行快速k近邻搜索。 这点在特征空间的维数大及训练数据容量大时尤其必要

线性扫描不可取,因为遍历所有的数据计算量太大

解决方法:kd树(kd tree)

kd树是二叉树, 平衡的kd树搜索时的效率未必是最优的

算法 3.2 (构造平衡 $k d$ 树)

输入: $k$ 维空间数据集 $T= \lbrace x_{1}, x_{2}, \cdots, x_{N} \rbrace$,其中 $x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(k)}\right)^{\mathrm{T}}$,$i=1,2, \cdots, N$
输出: $k d$ 树。
(1) 开始: 构造根结点,根结点对应于包含 $T$ 的 $k$ 维空间的超矩形区域。

​ $x^{(1)}$ 为坐标轴,以 $T$ 中所有实例的 $x^{(1)}$ 坐标的中位数为切分点,将根结点选择对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 $x^{(1)}$ 垂直的超平面实现。

​ 由根结点生成深度为 1 的左、右子结点: 左子结点对应坐标 $x^{(1)}$ 小于切分点的子$x^{(1)}$ 大于切分点的子区域。 区域,右子结点对应于坐标将落在切分超平面上的实例点保存在根结点。

​ (2)重复: 对深度为 $j$ 的结点,选择 $x^{(l)}$ 为切分的坐标轴,$l=j(\bmod k)+1,$ 以该结点的区域中所有实例的 $x^{(l)}$ 坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 $x^{(l)}$ 垂直的超平面实现。
​ 由该结点生成深度为 $j+1$ 的左、右子结点:左子结点对应坐标 $x^{(l)}$ 小于切分点 的子区域,右子结点对应坐标 $x^{(l)}$ 大于切分点的子区域。

​ 将落在切分超平面上的实例点保存在该结点。

​ (3)直到两个子区域没有实例存在时停止。从而形成 $k d$ 树的区域划分.

搜索kd树

给定一个目标点, 搜索其最近邻。 首先找到包含目标点的叶结点; 然后从该叶结点出发, 依次回退到父结点; 不断查找与目标点最邻近的结点, 当确定不可能存在更近的结点时终止

算法 $3.3$(用 $k d$ 树的最近邻搜索)

输入: 已构造的 $k d$ 树,目标点 $x$

输出: $x$ 的最近邻。

(1) 在 $k d$ 树中找出包含目标点 $x$ 的叶结点: 从根结点出发,递归地向下访问 $k d$ 树。若目标点 $x$ 当前维的坐标小于切分点的坐标,则移动到左子结点,否则移动到右子结点。直到子结点为叶结点为止

(2) 以此叶结点为“当前最近点”。

(3) 递归地向上回退,在每个结点进行以下操作:

​ (a)如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为“当前最近点”。

​ (b)当前最近点一定存在于该结点一个子结点对应的区域。检查该子结点的父结点的另一子结点对应的区域是否有更近的点。具体地,检查另一子结点对应的 区域是否与以目标点为球心、以目标点与“当前最近点”间的距离为半径的超球体相交。

​ 如果相交,可能在另一个子结点对应的区域内存在距目标点更近的点,移动 到另一个子结点。接着,递归地进行最近邻搜索;

​ 如果不相交,向上回退。

(4) 当回退到根结点时,搜索结束。最后的“当前最近点”即为 $x$ 的最近邻点。

3. 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
## max()函数技巧

## 初级技巧
tmp = max(1,2,4)
print(tmp)
#可迭代对象
a = [1, 2, 3, 4, 5, 6]
tmp = max(a)
print(tmp)

## 中级技巧:key属性的使用
# key参数不为空时,就以key的函数对象为判断的标准。
# 如果我们想找出一组数中绝对值最大的数,就可以配合lamda先进行处理,再找出最大值
a = [-9, -8, 1, 3, -4, 6]
tmp = max(a, key=lambda x: abs(x))
print(tmp)

## 高级技巧:找出字典中值最大的那组数据
#在对字典进行数据操作的时候,默认只会处理key,而不是value
#先使用zip把字典的keys和values翻转过来,再用max取出值最大的那组数据
#这个时候key是值,value是之前的key
#如果有一组商品,其名称和价格都存在一个字典中,可以用下面的方法快速找到价格最贵的那组商品:
prices = {
'A':123,
'B':450.1,
'C':12,
'E':444,
}
# 在对字典进行数据操作的时候,默认只会处理key,而不是value
# 先使用zip把字典的keys和values翻转过来,再用max取出值最大的那组数据
max_prices = max(zip(prices.values(), prices.keys()))
print(max_prices)
#这个时候key是值,value是之前的key
# (450.1, 'B')

#当字典中的value相同的时候,才会比较key
prices = {
'A': 123,
'B': 123,
}
max_prices = max(zip(prices.values(), prices.keys()))print(max_prices) # (123, 'B')
min_prices = min(zip(prices.values(), prices.keys()))print(min_prices) # (123, 'A')

K-NN

1
2
3
4
5
6
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline

from sklearn.datasets import load_iris
1
2
3
4
5
## load data
iris = load_iris() # np.array 格式
df = pd.DataFrame(iris.data, columns = iris.feature_names)
df['label'] = iris.target # 添加label列
df[:20] # 查看一下实际数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
data = np.array(df.iloc[:,:]) # 数据由Datafrom转化为array
X, Y = data[:, :-1], data[:, -1]

def _shuffle(X, Y):
randomize = np.arange(len(X))
np.random.shuffle(randomize)
return X[randomize], Y[randomize]
X, Y = _shuffle(X,Y)

def _train_test_split(X, Y, split_ratio = 0.1):
train_size = int(len(X) * (1 - split_ratio))
return X[:train_size], Y[:train_size], X[train_size:], Y[train_size:]

X_train, Y_train, X_test, Y_test = _train_test_split(X, Y, split_ratio = 0.1)
1
2
3
4
5
6
7
8
9
10
11
12
## 数据标准化
def _normalize(X, train_set = True, specified_columns = None, X_mean = None, X_std = None):
if specified_columns == None:
specified_columns = np.arange(len(X[0]))
if train_set:
X_mean = np.mean(X[:, specified_columns], axis = 0)
X_std = np.std(X[:, specified_columns], axis = 0)
X[:, specified_columns] = (X[:,specified_columns] - X_mean) / (X_std + 1e-8)
return X, X_mean, X_std

X_train, X_mean, X_std = _normalize(X_train, train_set=True)
X_test, _, _ = _normalize(X_test, train_set = None, X_mean = X_mean, X_std = X_std)
  • 线性遍历最近的K个点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class KNN_LinearSearch:
def __init__(self, X_train, Y_train, k = 3, p=2):
"""
k: 临近点的个数
p: 距离度量
"""
self.k = k
self.p = p
self.X_train = X_train
self.Y_train = Y_train

def predict(self, X_test):
"""
X_test,该函数找出最近的k个点,并按照多数表决规则进行预测
"""
result = []
for i in range(len(X_test)):
X = X_test[i]
knn_list = []
for j in range(self.k):
dist = np.linalg.norm(X - self.X_train[j], ord=self.p) #np中求线性代数范数的公式
knn_list.append((dist, self.Y_train[j]))
for l in range(self.k, len(self.X_train)):
max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))
dist_new = np.linalg.norm(X-self.X_train[l], ord=self.p)
if knn_list[max_index][0] > dist_new:
knn_list[max_index] = (dist, self.Y_train[l])

# 分类决策规则:多数表决
knn_label = [k[-1] for k in knn_list]
max_count_label = max(knn_label, key=knn_label.count)
result.append(max_count_label)
return result

def Correct_rate(self, X_test, Y_test):
right_count = 0
predictions = self.predict(X_test)
for i in range(len(Y_test)):
if predictions[i] == Y_test[i]:
right_count += 1
return right_count / len(X_test)
1
2
3
4
KNN = KNN_LinearSearch(X_train, Y_train)
KNN.predict(X_test)
# [1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0]
KNN.Correct_rate(X_test, Y_test)
  • Kd树-最近邻搜索
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class KdNode:
def __init__(self, dom_elt, split, left, right):
self.dom_elt = dom_elt # k维向量节点(k维空间中的一个样本点), 分割的那个样本点 dom_elt = [1,2,3,5,6...]
self.split = split # 整数(进行分割纬度的序号)
self.left = left # 该结点分割超平面左子空间构成的kd-tree
self.right = right # 该结点分割超平面右子空间构成的kd-tree

class KdTree:
def __init__(self, data):
dim = len(data[0]) # 数据维度

def CreateNode(split, data_set):
if not data_set:
return None
# 按要进行分割的那一切数据排序
data_set.sort(key=lambda x: x[split]) # 这里sort,key的用法和max类似
split_pos = len(data_set) // 2 # 整数除法
median = data_set[split_pos] # 中位数分割点
split_next = (split + 1) % dim

# 递归的创建kd树
return KdNode(median, split,
CreateNode(split_next, data_set[:split_pos]),
CreateNode(split_next, data_set[split_pos + 1:]))
self.root = CreateNode(0, data) # 从第0维开始创建kd树

# kd树的前序遍历
def preorder(root):
print(root.dom_elt)
if root.left: # 节点不为空
preorder(root.left)
if root.right:
preorder(root.right)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
## 寻找最近的k个点
from math import sqrt
from collections import namedtuple

# 定义一个namedtuple, 分别存放最近坐标点、最近距离和访问过的节点数
result = namedtuple("Result_tuple", "nearest_point nearest_dist nodes_visited")

def find_nearest(tree, point):
dim = len(point)
def travel(kd_node, target, max_dist):
# 迭代终止条件
if kd_node is None:
return result([0] * dim, float("inf"), 0) # float("inf")正无穷,float("-inf")

nodes_visited = 1
s = kd_node.split # 分割的纬度
pivot = kd_node.dom_elt # 进行分割的轴,实例点

if target[s] <= pivot[s]: # 如果目标点第s维小于分割点的对应值
nearer_node = kd_node.left #下一访问节点为左子树根节点
further_node = kd_node.right # 同时记录右子树以便后期与目标点进行比较
else:
nearer_node = kd_node.right
further_node = kd_node.left

temp1 = travel(nearer_node, target, max_dist) # 递归遍历

nearest = temp1.nearest_point # 以此叶节点作为当前最近点
dist = temp1.nearest_dist # 更新最近距离
nodes_visited += temp1.nodes_visited

if dist < max_dist:
max_dist = dist

temp_dist = abs(pivot[s] - target[s]) # 第s维上目标点与分割超平面的距离

if max_dist < temp_dist: # 判断超球体是否与超平面相交
return result(nearest, dist, nodes_visited) # 不相交则可以直接返回,不用继续判断,也就意味这父节点(分割点)的另一子节点不会比现在的子节点与目标点更近

# 计算目标点和分割点的欧氏距离
temp_dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(pivot, target)))
if temp_dist < dist: # 意味着分割点此时为最近点
nearest = pivot # 更新最近点
dist = temp_dist # 更新最近距离
max_dist = dist # 更新超超球体半径

# 检查另一个子节点对应的区域是否有更近的点
temp2 = travel(further_node, target, max_dist)

nodes_visited += temp2.nodes_visited

if temp2.nearest_dist < dist: # 如果另一个子节点内存在更近距离
nearest = temp2.nearest_point
dist = temp2.nearest_dist

return result(nearest, dist, nodes_visited)
return travel(tree.root, point, float("inf")) # 从根节点递归
1
2
3
4
5
6
7
8
9
data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
kd = KdTree(data)
preorder(kd.root)
[7, 2]
[5, 4]
[2, 3]
[4, 7]
[9, 6]
[8, 1]
1
2
3
4
5
6
7
8
9
10
from time import clock
from random import random

# 产生一个k维随机向量,每维分量值在0~1之间
def random_point(k):
return [random() for _ in range(k)]

# 产生n个k维随机向量
def random_points(k, n):
return [random_point(k) for _ in range(n)]
1
2
3
ret = find_nearest(kd, [3, 4.5])
print(ret)
# Result_tuple(nearest_point=[2, 3], nearest_dist=1.8027756377319946, nodes_visited=4)
1
2
3
4
5
6
7
8
9
N = 400000
t0 = clock()
kd2 = KdTree(random_points(3,N)) # 构建包含四十万个3维空间样本点的kd树
ret2 = find_nearest(kd2, [0.1, 0.5, 0.8]) # 四十万个样本点中寻找离目标最近的点
t1 = clock()
print("time: ", t1-t0, "s")
print(ret2)
# time: 5.106322 s
# Result_tuple(nearest_point=[0.10488218676493222, 0.49793482149466295, 0.7951887681764834], nearest_dist=0.007158817047962914, nodes_visited=62)

Reference:

[1] https://github.com/fengdu78/lihang-code/blob/master/%E7%AC%AC03%E7%AB%A0%20k%E8%BF%91%E9%82%BB%E6%B3%95/3.KNearestNeighbors.ipynb

[2] 《统计学习方法》 —李航

二分类线性模型

1. 模型

$$
f(x)=\operatorname{sign}(w \cdot x+b)
$$

Loss function:
$$
L(w, b)=-\sum_{x_{i} \in M} y_{i}\left(w \cdot x_{i}+b\right)
$$
因此其目标函数
$$
\min_{w, b}L(w, b)=-\sum_{x_{i} \in M} y_{i}(w \cdot x_{i}+b)
$$
优化方法:

  • 随机梯度下降(SGD):每次更新使用一个数据

算法2.1:

输入:训练数据集 $T = {(x_1,y_1),(x_2,y_2),…,(x_N,y_N)}$ ,其中 $x_{i} \in \mathcal{X}=\mathbf{R}^{n}$,$y_{i} \in \mathcal{Y}=\lbrace -1,+1 \rbrace$, $i=1,2, \cdots, N $ ;学习率 $\eta (0<\eta \leqslant 1) ;$
输出 :$ w, b $ 知机模型 $f(x)=\operatorname{sign}(w \cdot x+b)$ 。
(1) 选取初值 $w_{0}, b_{0} ;$
(2) 在训练集中选取数据 $\left(x_{i}, y_{i}\right) ;$
(3) 如果 $y_{i}\left(w \cdot x_{i}+b\right) \leqslant 0$,
$$
\begin{array}{l}
w \leftarrow w+\eta y_{i} x_{i} \\
b \leftarrow b+\eta y_{i}
\end{array}
$$
(4) 转至 (2),直至训练集中没有误分类点。

2. 算法的收敛性

对于线性可分的的数据集,perceptron经过有限次迭代一定会收敛

证明过程待后续补充:

3. 感知机的对偶形式

对偶形式的基本想法是,将 $w$ 和 $b$ 表示为实例 $x_{i}$ 和标记 $y_{i}$ 的线性组合的形式, 通过求解其系数而求得 $w$ 和 $b$ 。不失一般性,在算法中可假设初始值 $w_{0}, b_{0}$ 均为
$0 。$ 对误分类点 $\left(x_{i}, y_{i}\right)$ 通过
$$
\begin{array}{l}
w \leftarrow w+\eta y_{i} x_{i} \\
b \leftarrow b+\eta y_{i}
\end{array}
$$
最后学得的 $w,b$ 可以表示为
$$
\begin{array}{c}
w=\sum_{i=1}^{N} \alpha_{i} y_{i} x_{i} \\
b=\sum_{i=1}^{N} \alpha_{i} y_{i}
\end{array}
$$
算法 2.2(感知机学习算法的对偶形式)
输入:线性可分的数据集 $T = \lbrace (x_1,y_1),(x_2,y_2),…,(x_N,y_N) \rbrace$, 其中 $x_{i} \in \mathbf{R}^{n}$,$y_{i} \in\lbrace -1,+1 \rbrace, i=1,2, \cdots, N $ 学习率 $\eta(0<\eta \leqslant 1) ;$
输 出:$\alpha, b$; 感 知 机 模型 $f(x)=\operatorname{sign}\left(\sum_{j=1}^{N} \alpha_{j} y_{j} x_{j} \cdot x+b\right), \quad$ 其 中 $\alpha=\left(\alpha_{1}, \alpha_{2}, \cdots, \alpha_{N}\right)^{\mathrm{T}}$
(1) $\alpha \leftarrow 0, b \leftarrow 0 ;$
(2) 在训练集中选取数据 $\left(x_{i}, y_{i}\right) ;$

(3) 如果 $y_{i}\left(\sum_{j=1}^{N} \alpha_{j} y_{j} x_{j} \cdot x_{i}+b\right) \leqslant 0$,
$$
\begin{array}{l}
\alpha_{i} \leftarrow \alpha_{i}+\eta \\
b \leftarrow b+\eta y_{i}
\end{array}
$$
(4)转至 (2) 直到没有误分类数据。

讨论感知机的对偶有什么意义?

这两个不应该是一样的吗?

4. 感知机代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import numpy as np
import pandas as pd
np.random.seed(0)
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data[:100, :2]
Y = iris.target[:100]
Y[Y == 0] = -1

# 数据标准化
def _normalize(X, train_set = True, specified_column = None, X_mean = None, X_std = None):
if specified_column == None:
specified_column = np.arange(len(X[0]))
if train_set:
X_mean = np.mean(X[:, specified_column], axis = 0)
X_std = np.std(X[:, specified_column], axis = 0)
X[:, specified_column] = (X[:, specified_column] - X_mean) / (X_std + 1e-8)
return X, X_mean, X_std

X, X_mean, X_std = _normalize(X, train_set = True)

## 注意label的标枪必须是 -1 和 1 要不然会无限循环下去
## 这样写有个错误,如果数据集不是线性可分的,将一直循环下去, iris数据集是线性可分的
def _train(X, Y, learning_rate = 0.2):
data_dim = len(X[0])
w = np.zeros((1, data_dim))
b = 0
not_finish = True
while not_finish:
wrong_count = 0
for idx in range(len(X)):
Y_pred = np.sign(np.dot(w, X[idx]) + b)
if Y_pred * Y[idx] <= 0:
w += learning_rate * Y[idx] * X[idx]
b += learning_rate * Y[idx]
wrong_count += 1
if wrong_count == 0:
not_finish = False
return 'Perceptron is done!'
_train(X,Y,0.1)

1. 有/无模型学习

  • 有模型学习

    方法:动态规划

  • 无模型

    方法:蒙特卡洛、时序差分

2. 同/异策学习

  • 同策回合更新(策略$\pi$和环境交互得到==完整轨迹==,然后学习更新价值函数$V^{\pi}$)
    • 每次访问回合更新
    • 首次访问回合更新
    • 带起始探索的同策回合更新(避免陷入局部最优)
    • 基于柔性策略的同策回合更新(避免陷入局部最优)
  • 异策回合更新行为策略$\pi’$和环境交互得到==完整轨迹==,目标策略$\pi$学习更新价值函数$V^{\pi}$)
    • 重要性采样 李宏毅讲的很好
    • 异策回合更新策略评估 # 利用行为策略估计目标策略的动作状态价值对函数和状态价值对函数
    • 异策回合更新最优策略的求解 # 评估 + 改进 改进部分是依据每个状态下的最大价值动作。

3. 回合更新/时序差分更新

  • 回合更新(无偏差但方差大,和环境完整的交互一个回合)
    • 利用蒙特卡洛方法,对完整的G进行无偏差估计,但是方差较大
  • 时序差分更新(有偏差,但方差小,收敛快,n步就和环境交互n次)
    • 同策,策略$\pi$生成轨迹,然后策略$\pi$学习更新$q(s,·)$,然后根据q值更新策略
      • $TD(\lambda)$(除极端的无穷外是时序差分,同策)
      • SARSA算法 = 单/多步时序差分(策略评估)+ 策略改进 (同策)
      • 期望SARSA算法:$U_t=R_{t+1}+\gamma \sum_{a\in \mathcal{A}}\pi(a|S_{t+1})q(S_{t+1},a)$ 也有单步和多步 (同策)
    • 异策,行为策略$b$和环境进行交互得到轨迹,然后依据此轨迹对策略$\pi$进行学习更新q值,然后根据q值对策略进行更新
      • 单步/多步时序差分
      • SARSA算法/期望SARSA算法
      • Q-learning
      • Double Q-learning
  • 资格迹
    • 资格迹和以上时序差分策略都可以结合,不管是同策还是异策。

4. 基于价值/策略/二者结合

  • 基于价值

    • 蒙特卡洛方法估计G
    • 动态规划
    • $TD(\lambda)$
    • SARSA
    • Q-learning
    • Double Q-learning
  • 基于策略(回合更新,利用策略交互不断得到完整轨迹,多个回合更新目标是使策略$\pi$的期望$E(G)$最大

    • 同策回合更新策略梯度算法
    • 异策回合更新策略梯度算法
  • 执行者/评论者方法 = 价值更新 + 策略更新 (价值更新部分使用深度强化学习方法+单步时序差分,策略更新也是用深度强化学习方法)

    • 动作价值执行者评论者算法 $E(\Psi_t \nabla \ln\pi(a_t|s_t;\theta)), \Psi_t=\gamma^tq_{\pi}(s_t,a_t)$

    • A2C-优势执行者/评论者算法 $\Psi_t=\gamma^t[q_{\pi}(s_t,a_t)-v_{\pi}(s_t)]$

    • 时序差分执行者/评论者算法 $\Psi_t=\gamma^t[r_{t+1}+\gamma v_{\pi}(s_{t+1})-v_{\pi}(s_t)]$

    • A3C-异步优势执行者/评论者算法;利用多个agent同时搜集经验更新自己的参数并传给一个总的agent,可以单/多步时序差分

    • 资格迹的执行者评论者算法

    • 基于代理优势的同策算法

      • 邻近策略优化PPO2,在目标中添加描述分布差异的指标,$E_{\pi(\theta_{old})}[\min(\frac{\pi(A_t|S_t;\theta_{new})}{\pi(A_t|S_t;\theta_{old})}a_{\pi(\theta_{old})}(S_a,A_t), a_{\pi(\theta_{old})}(S_a,A_t)+\varepsilon|a_{\pi(\theta_{old})}(S_a,A_t)|)]$
    • 信任域算法

      • 自然策略梯度算法NPG,最大化代理优势,并添加约束条件,KL散度小于$\delta$,但NPG是对目标函数进行泰勒一阶近似,约束条件进行泰勒二阶近似。对近似后的问题进行求解。

        $\mathbf{\theta} \leftarrow \mathbf{\theta} + \sqrt{\frac{2\delta}{\mathbf{g^TF^{-1}g}}}\mathbf{F^{-1}g}$

      • 带共轭梯度的自然策略梯度算法

        利用共轭梯度算法求解$\mathbf{F^{-1}g},\mathbf{Fx=g}$,$\mathbf{\theta} \leftarrow \mathbf{\theta} + \sqrt{\frac{2\delta}{\mathbf{x^TFx}}}\mathbf{x}$

      • 信赖域策略优化TRPO,在NPG的基础上修改得到,因为其近似问题可能不一定导致问题有最优解,在个别情况下可能使情况更糟,TRPO对迭代式进行改进
        $$
        \boldsymbol{\theta_{k+1} = \theta_k}+\alpha^j \sqrt{\frac{2\delta}{\boldsymbol{[x([\theta_k])^T]Fx([\theta_k])}}}\boldsymbol{x([\theta_k])}
        $$
        其中$\alpha$是学习参数,$j$是某个非负整数,对于自然策略梯度$j$总是0,TRPO用一下方法确定$j$的值:从非负整数d到0,1,2…中依次寻找首个满足期望KL散度约束并且能够提升代理梯度的值。不过由于一般近似的不错,所以一般为0,个别情况为1,其他值几乎没有。

      • Kronecker因子信任域执行者/评论者算法 ACKTR,将Kronecker因子近似曲率算法用到信任域策略优化算法中,减少计算量

    • 重要性采样异策执行者/评论者算法

      • 异策执行者/评论者算法,将目标期望$E_{\pi(\boldsymbol{\theta})}(\Psi_t \nabla \ln\pi(a_t|s_t;\theta))$,变为$E_b(\frac{\pi(A_t|S_t;\boldsymbol{\theta})}{b(A_t|S_t)}\Psi_t \nabla \ln\pi(a_t|s_t;\theta))$
      • 带经验回放的异策执行者/评论者算法(ACER
    • 柔性执行者/评论者算法(SAC)

    • 确定性策略

      • 同策确定性算法
      • 异策确定性算法
      • 深度确定性策略梯度算法(DDPG)
      • 双重延迟深度确定性策略梯度算法(TD3)

5. 深度强化学习/非深度强化学习

  • 深度Q学习
  • 双重深度Q网络
  • 对偶深度Q网络
    • 对于某个状态S,有些action可能采样不到,但通过对偶深度Q网络也可以更新
  • Noise Net
    • 对采样的Q网络的参数添加噪声,探索
  • Distributional Q_function
    • 该方法认为Q(s, a)有一个分布,即相同的状态和行为的价值,是一个分布,用神经网络估计这个分布

6. 离线学习/在线学习(待更)

如何理解资格迹

考虑这样一个回合$S_0,A_0,R_1,S_1,A_1,…,R_n,S_n,A_n$

我们知道对于 $(S_0,A_0)$计算$q_(S_0,A_0)$,可以用$U(S_0,A_0)=R_1+q(S_1,A_1)$近似为真实值去更新$q_(S_0,A_0)$,这个时候时序误差为$TD_{error}=U(S_0,A_0)-q(S_0,A_0)$,时序误差所包含的信息全部用来更新$q_(S_0,A_0)$

$(S_5,A_5)$时,

$U(S_5,A_5)=R_6+\gamma q(S_6,A_6)$

$U(S_0,A_0)=R_1+\gamma R_2+\gamma ^2R_3+\gamma ^3R_4+\gamma ^4R_5+\gamma ^5U(S_5,A_5) $

这个时候$(S_5,A_5)$处的时序误差对于更新 $(S_0,A_0)$处的$q_(S_0,A_0)$所产生的作用是$\gamma ^5 *TD_{error}$

从上式可以看出与$(S_0,A_0)$状态相距越远的$(S_k,A_k)$所产生的时序误差对于$(S_0,A_0)$所起的作用就越小,所以间隔步数每增加一步,前面的每个动作状态对的资格迹都要进行衰减.

比如在$t=4$时,

$U(S_0,A_0):\gamma^4,U(S_1,A_1):\gamma^3,U(S_2,A_2):\gamma^2,U(S_3,A_3):\gamma$。

前进一步,$t=5$时,

$U(S_0,A_0):\gamma^5,U(S_1,A_1):\gamma^4,U(S_2,A_2):\gamma^3,U(S_3,A_3):\gamma^2,U(S_4,A_4):\gamma$

而对于$TD(\lambda)$ 来说

$U(S_0,A_0)=R_1+q(S_1,A_1)\qquad 1-\lambda$

$U(S_0,A_0)=R_1+\gamma U(S_1,A_1) \qquad (1-\lambda)\lambda$

$U(S_0,A_0)=R_1+\gamma R_2+\gamma ^2 U(S_2,A_2) \qquad (1-\lambda)\lambda ^2$

…….

所以其间隔步数每增加一步,当前状态动作价值产生的时序误差,用到更新前面的状态动作价值时要相应的衰减对应$\gamma\lambda$次幂。

考虑资格迹其实看起来很合理,因为当回合轨迹逐渐延伸,不仅当前的动作状态价值需要更新,由于新的信息的出现,又会使得之前已经更新的动作状态价值,和我们的$U$(类似机器学习的目标)之间重新产生新的误差,我们理应将这部分考虑到损失函数中,进行梯度更新。

带资格迹的梯度下降

《强化学习导论》Richard S.Sutton:资格迹$\mathbf{z}_t$是一个和权值向量$\mathbf{w}_t$ 同维度的向量,在$TD(\lambda)$中,资格迹向量被初始化为零,然后在每一步累加价值函数的梯度,以$\gamma\lambda$

$$
\begin{align}
\mathbf{z_{-1}} &= \mathbf{0}, \\
\mathbf{z_t} &= \gamma\lambda\mathbf{z_{t-1}}+\nabla\hat{v}(S_t,\mathbf{w_t}), 0 \leq t\leq T
\end{align}
$$
某一时刻的单步时序差分误差为
$$
\delta_t=R_{t+1}+\gamma\hat{v}(S_{t+1},\mathbf{w_t})-\hat{v}(S_t,\mathbf{w_t})
$$
在$TD(\lambda)$中,权值向量每一步的更新正比于时序差分的标量的误差和资格迹。
$$
\mathbf{w_{t+1}}=\mathbf{w_t}+\alpha\delta_t\mathbf{z_t}
$$
当时看资格迹时候特别迷这一部分,其实自己稍微推导两个回合就明白了。这里的资格迹其实就是把后面的梯度中的学习率和时序误差提出来剩下的内容。因为是利用函数近似,所以会包含价值的梯度。具体推导两步。

当$t=0$时:
$$
\begin{align*}
U(S_0) &= R_1+\gamma v(S_1,\mathbf{w_0}) \\
Loss &= [U(S_0)-v(S_0,\mathbf{w_0})]^2 \\
\mathbf{w_1} &= \mathbf{w_0}+\alpha[U(S_0)-v(S_0,\mathbf{w_0})]\nabla v(S_0,\mathbf{w_0}) \\
&= \mathbf{w_0}+\alpha\delta_0\nabla v(S_0,\mathbf{w_0}) \\
\mathbf{z_0} &=\mathbf{0} + \nabla v(S_0,\mathbf{w_0})
\end{align*}
$$
从上式资格迹可以看出,其和权值向量是同维度的。

当$t=1$时:
$$
U(S_1)=R_2+\gamma v(S_2,\mathbf{w_1})
$$
这时时序误差为$\delta_1=U(S_1)-v(S_1)$,而$U(S_0) = R_1 + \gamma U(S_1)$,所以在$TD(\lambda)$下,$U(S_0)$和$v(S_0,\mathbf{w_1})$的误差为$\gamma\lambda\delta_1$,这两部分的误差理应都考虑,这个时候的损失函数就变为
$$
Loss=[U(S_0)-v(S_0,\mathbf{w_1})]^2 + [U(S_1)-v(S_1,\mathbf{w_1})]^2
$$
进行梯度更新
$$
\begin{align*}
\mathbf{w_2} &= \mathbf{w_1}+\alpha[U(S_0)-v(S_0,\mathbf{w_1})]\nabla v(S_0,\mathbf{w_1})+ \alpha[U(S_1)-v(S_1,\mathbf{w_1})]\nabla v(S_1,\mathbf{w_1})\\
&= \mathbf{w_1}+\alpha\gamma\lambda\delta_1\nabla v(S_0,\mathbf{w_1}) +\alpha\delta_1\nabla v(S_1,\mathbf{w_1})\\
\end{align*}
$$
把学习率和时序误差$\delta _1$提出来,得到资格迹
$$
\mathbf{z_1} =\gamma\lambda\nabla v(S_0,\mathbf{w_1}) + \nabla v(S_1,\mathbf{w_1})
$$
由于$\nabla v(S_0,\mathbf{w_0})=\nabla v(S_0,\mathbf{w_1})$,所以
$$
\mathbf{z_1} =\gamma\lambda\mathbf{z_0} + \nabla v(S_1,\mathbf{w_1})
$$
以次往下更新……