-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfeed.xml
More file actions
1265 lines (923 loc) · 113 KB
/
feed.xml
File metadata and controls
1265 lines (923 loc) · 113 KB
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
<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="/github-io/feed.xml" rel="self" type="application/atom+xml" /><link href="/github-io/" rel="alternate" type="text/html" /><updated>2024-07-20T13:01:40+08:00</updated><id>/github-io/feed.xml</id><title type="html">Homepage of Jiajun Li</title><subtitle>Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description.</subtitle><author><name>Jiajun Li</name></author><entry><title type="html">记配置n遍spark多机分布式环境</title><link href="/github-io/2022-03-02/Spark-01" rel="alternate" type="text/html" title="记配置n遍spark多机分布式环境" /><published>2022-03-02T00:00:00+08:00</published><updated>2022-03-02T00:00:00+08:00</updated><id>/github-io/2022-03-02/Spark-01</id><content type="html" xml:base="/github-io/2022-03-02/Spark-01"><![CDATA[<h2 id="背景">背景</h2>
<p>最近由于论文的关系,设计的算法需要在分布式环境下,测试算法的通信时间通信代价,于是尝试配置了多台机器的分布式环境。由于配置过程较为复杂,其中也遇到许许多多问题,由于各式各样的因素,不得不一直转换不同的环境,完成机器的配置。虽然由于水平不足,犯了许多不必要的配置错误,有的问题看起来比较愚蠢,但为了之后避免踩入相同的坑,也就将这一路以来,不断配置更新的过程写成文章,以方便查找。</p>
<h2 id="配置20遍">配置20遍</h2>
<p>最初使用的平台是人大校级计算平台,在这个平台上,可以申请一定数量的机器,然后以科研结果作为经费抵扣。使用此平台的原因是之前有前辈在上面配置过 Spark 环境,而我有一定机会可以直接利用他配置好的成果,然而事情并没有像我想象的那么简单。此时出现了两个主要的问题,其一是该环境并没有真正配置yarn,并不能做到真正的并行;其次实际上此平台的集群是在一个大机器上分割出的小虚拟机组成集群,这样的集群实际上的通信代价是非常低的,这无法体现出我们算法的优势,因此我不得不寻找其他平台。之后就在组里先找了6台服务器,直接利用这6台服务器搭建一个集群,虽然机器数目少一点,但平摊下来,每个机器都比原来的配置要更好。当然事情不会那么顺利,由于我实验操作的数据量极大,我不断试探服务器计算能力的上限,最终这些服务器也难堪重负,纷纷内存耗尽、磁盘耗尽,引发了一系列不好的连锁反应,究其原因是我没有做docker环境隔离(要学的东西还很多)。由于当时论文ddl在即,让我只能在夜间跑代码,完全是不可能完成目标的,因此我不得不使用阿里云下的服务器。之后就搞了16台阿里云服务器,并在上面配置真·分布式环境,此时我已经有了十次左右配置环境的经验,但哪怕如此,又经历了经费不足、神秘bug等等意想不到的问题,但我最终还是勉强完成了论文,初次投稿当然还是被拒了。之后改投论文的过程中,吸取了服务器可能很容易崩,随时可能换机器的现实,尽可能地将许多作业改为了批处理,终于又配置了很多次,最终完成了实验和论文。</p>
<h2 id="分布式环境的成分">分布式环境的成分</h2>
<h3 id="hdfs">HDFS</h3>
<p>虽然说使用 Spark without Hadooop 从一定程度上配置或许会简单一点,但为了比较清晰地感受分布式环境,并更好地存储数据,我还是采用了 Hadoop 与 Spark 分开配置的策略,这里使用的 Hadoop 版本为 3.3.1。</p>
<p>(HDFS 其实就是一个分布式的文件管理系统,将数据分布式的存储在不同的机器上,一方面可以存的更多,一方面也是可以使得处理数据更快,数据直接分布在不同机器上,也就省去了从主机向其他机器发送数据的通信过程。)</p>
<h3 id="spark">SPARK</h3>
<p>这里使用Spark的版本是3.1.2
(Spark 分布式计算的环境,利用这样已有的环境就不需要自己去写通信、底层调度,也不必担心各种死锁的问题。)</p>
<p>由于我是不太会 Scala 的(但是任意一种语言,稍微看看基础代码我还能做到),为了方便上手,这里使用的是 PySpark, PySpark 是python环境下提供的spark接口,这样只需要掌握好启动命令,再学一些简单的语法,就可以将spark 调动起来。</p>
<h3 id="其他部分">其他部分</h3>
<p>JDK 自然是必不可少的,我这里用的是1.8.0,scala也需要装一下,我用的是2.12.11。</p>
<h2 id="多机">多机</h2>
<h3 id="多机免密">多机免密</h3>
<p>要想真正把多台机器运行起来,首先需要的是保证多台机器之间不再需要手动输入密码。虽然两台机器配置免密登陆很简单,但多台机器要保证免密登陆,靠手动收发文件就变得十分磨人,16台机器就要操作 16^2 次。这里网上有各种教程,我只提一下核心思路,不想放太多代码。核心思想是先在文件中按照一定格式存放好服务器的信息(分为主节点和从节点,为了方便,一般可以让所有机器之间互相通信),先尝试能否批量访问每个服务器,再批量生成证书,并将证书分发。
利用以下命令,清除 known_hosts 文件</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh-keygen -f '~/.ssh/known_hosts' -R ip地址
</code></pre></div></div>
<p>利用以下命令,将证书分发到服务器:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh-copy-id -i ~/.ssh/id_rsa.pub -p 端口号 用户名@ip地址
</code></pre></div></div>
<p>利用 fabric 包的 Connection 函数,创建 host ,连接服务器。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>host = Connection(ip地址, port = 端口, user=用户名,connect_kwargs={'password':密码,'timeout':10})
</code></pre></div></div>
<p>在创建了 host 以后,即可利用 host 执行清除 known_hosts 的命令、分发证书的命令,以及其他各式各样的命令。
下面给出例子:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>result = host.run(需要执行的命令,pty=True,watchers=[passwd,yes],hide=True,warn=True,timeout=60)
</code></pre></div></div>
<p>这里的pty是指伪终端,不设置的话,有的命令会失败;watchers解决的是,当遇到不同的反馈时,需要作出的反馈,例子如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>passwd = Responder(
pattern=r'password',
response=node.passwd + '\n'
)
yes = Responder(
pattern = r'(yes/no)',
response = 'yes\n'
)
</code></pre></div></div>
<p>利用正则表达式,根据不同条件下返回的字符串,输入不同的应答,这样就可以做到自动化应答。</p>
<p>这里也是参考不少网络上的<a href="https://blog.csdn.net/qq_28721869/article/details/115094788">教程</a>,遇到问题,只需要把握原理,基本就能解决。当实在解决不了时,可以切换到手动模式,重新进行调试。</p>
<h3 id="基于一台机器的多机收发">基于一台机器的多机收发</h3>
<p>这里的多机收发目的是希望批量将机器的文件收发到不同机器上。利用上一节的host,只能创建一个虚拟的终端,不太适合收发文件。从一个服务器往其他服务器传输 Java、Spark、hosts等文件,并不需要远程登录其他服务器,执行传输文件命令即可。
其次,可以从每个服务器中,收集它们的信息,并在同一个机器中整合,在后面提到的 bug,正是需要从每个机器中收集信息。</p>
<p>如果配置好了免密登陆,那么可以直接利用os命令,直接执行本机的命令,例如文件接收:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>os.system('scp -P 端口号 用户名@ip地址:目标文件地址 本地目标地址')
</code></pre></div></div>
<p>如果没有配置好密码,可以使用 pexpect 包,发密码并执行命令,例如从本地往服务器中收发文件,例子如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>scp_crt_command = 'scp -P 端口号 用户名@ip地址:目标文件地址 本地目标地址'
child = pexpect.spawn(scp_crt_command)
child.expect(r'password')
child.sendline(node.passwd)
child.read()
</code></pre></div></div>
<p>这一节的命令胜在简单,在不想写太多代码时,可以利用尽可能短的方式,完成代码的收发工作。当然,若是想在每台机器上执行一定的命令,还是需要采用上一小节的方式。</p>
<h2 id="分布式环境的配置">分布式环境的配置</h2>
<p>HDFS + Spark 的配置文件,只需要完成一次,并保存好一份副本,即可在服务器失效的情况下,快速再完成配置。这里的配置也可以参考许多博客中说的,多机配置 HDFS+Spark 环境,最好需要配置yarn,以保证分布式的运行。其中有太多的细节,基本上也是根据官网提供的流程走。</p>
<p>首先是配置.bashrc文件,将scala、hdfs、spark、jre等的路径配置好,我这里给出配置的例子:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export JAVA_HOME=/root/spark/jdk1.8.0_301
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib:$CLASSPATH
export PATH=$PATH:/root/spark/jdk1.8.0_301/bin
export PATH=$PATH:/root/spark/scala-2.12.11/bin
export HADOOP_HOME=/root/spark/hadoop-3.3.1
export PATH=$PATH:${HADOOP_HOME}/bin
export SPARK_HOME=/root/spark/spark-3.1.2
export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native
</code></pre></div></div>
<p>利用文件收发可以批量将这一段代码以文件的形式发送到每个机器上,再使用免密登陆部分的代码,使每个机器将这些代码写入到每个机器的 .bashrc 文件中。Scala和Java的安装只需要下好包,并设置.bashrc即可。HDFS和Spark则需要改一些配置文件。</p>
<h3 id="hdfs-1">HDFS</h3>
<p>HDFS 的配置主要修改 etc/hadoop/core-site.xml、etc/hadoop/hdfs-site.xml,要配置YARN的时候需要配置etc/hadoop/mapred-site.xml、etc/hadoop/yarn-site.xml文件,配置的内容可以参考 hadoop 的<a href="https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/SingleCluster.html">官网</a>。
这里给出我的配置方案:
首先配置 worker 文件,我有16台机器,则配置了16个worker:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#workers
Slave01
# 这里代表中间的13个节点,写作workerXX
Slave15
</code></pre></div></div>
<p>要想顺利使用 Master-Slave 或者是 Main-Worker,亦或者是其他主从名字,都需要在将 ip 地址于设置的名字一一对应,并写入到/etc/hosts 文件中。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># core-site.xml
<configuration>
<property>
<name>hadoop.tmp.dir</name>
<value>/mnt/tmp</value>
</property>
<property>
<name>fs.defaultFS</name>
<value>hdfs://Master:9000</value>
</property>
</configuration>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># hdfs-site.xml
<configuration>
<property>
<name>dfs.replication</name>
<value>2</value>
</property>
<property>
<name>dfs.namenode.secondary.http-address</name>
<!--value>Master:50090</value-->
<value>Master:50090</value>
</property>
<property>
<name>dfs.namenode.name.dir</name>
<value>file:/mnt/tmp/hadoop/dfs/name</value>
</property>
<property>
<name>dfs.datanode.data.dir</name>
<value>file:/mnt/tmp/hadoop/dfs/data</value>
</property>
<property>
<name>dfs.wenhdf.enabled</name>
<value>true</value>
</property>
</configuration>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#mapred-site.xml
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
<property>
<name>mapreduce.jobhistory.address</name>
<value>Master:10020</value>
</property>
<property>
<name>mapreduce.jobhistory.webapp.address</name>
<value>Master:19888</value>
</property>
</configuration>
</code></pre></div></div>
<p>这里可以利用其中一个节点分担主节点的管理压力,也可以都设置为 Master</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#yarn-site.xml
<configuration>
<property>
<name>yarn.resourcemanager.hostname</name>
<value>Slave01</value>
</property>
<property>
<name>yarn.nodemanager.aux-services.mapreduce.shuffle.class</name>
<value>org.apache.hadoop.mapred.ShuffleHandler</value>
</property>
<property>
<name>yarn.resourcemanager.address</name>
<value>Slave01:8032</value>
</property>
<property>
<name>yarn.resourcemanager.scheduler.address</name>
<value>Slave01:8030</value>
</property>
<property>
<name>yarn.resourcemanager.resource-tracker.address</name>
<value>Slave01:8031</value>
</property>
<property>
<name>yarn.resourcemanager.admin.address</name>
<value>Slave01:8033</value>
</property>
<property>
<name>yarn.resourcemanager.webapp.address</name>
<value>Slave01:8088</value>
</property>
</configuration>
</code></pre></div></div>
<p>只有真正配置好yarn,在执行任务的过程中,才能真正调度好每台机器的CPU,否则计算只是本地模式,无法做到真正测试分布式的目的。</p>
<p>在完全配置好Hadoop之后,将相同的内容分发到其他机器上,然后再执行 sbin/start-all.sh 将整个服务调动起来,也可以通过执行每个部分的start文件,开始部分服务。但要保证HDFS正确的执行,还需要重要的一步:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bin/hdfs namenode -format
</code></pre></div></div>
<p>这一步的作用是将namenode初始化,当服务不小心宕掉后,或是服务器关机多时,有可能导致HDFS无法重启服务时,也需要删除Hadoop中的缓存文件,再重新运行此命令,使服务重启。至于Hadoop临时文件的位置,则设置在了 core-site.xml 的属性 hadoop.tmp.dir 里。而我为了保证当缓存过多时,不影响服务器的正常执行,将临时目录挂在了其他的硬盘上——/mnt/tmp,这里也需要因人而异。</p>
<p>最后,可以利用jps命令查看各部分服务是否启动成功,并依据没启动成功的部分,查找相应位置的配置。总的来说,HDFS的配置的坑并不算多,依照网上的教程来,难度算是比较适中的。</p>
<h3 id="spark-1">Spark</h3>
<p>Spark的配置,其实也不算难,但由于它在最上层,下层出错之后,往往难以定位,总是认为是Spark配置的问题,这会导致很难查找错误。Spark的配置主要关注于 conf/spark-env.sh 、conf/workers, conf/workers没太多好说的,跟Hadoop配置的一样即可。我给出我配置 spark-env.sh 的结果:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>export HADOOP_HOME=/root/spark/hadoop-3.3.1
export HADOOP_CONF_DIR=/root/spark/hadoop-3.3.1/etc/hadoop
export JAVA_HOME=/root/spark/jdk1.8.0_301
export SCALA_HOME=/root/spark/scala-2.12.11
export SCALA_LIBRARY_PATH=${SCALA_HOME}/lib
export SPARK_WORKING_MEMORY=60g
export SPARK_MASTER_IP=Master
export SPARK_DIST_CLASSPATH=$(/root/spark/hadoop-3.3.1/bin/hadoop classpath)
export LD_LIBRARY_PATH=$HADOOP_HOME/lib/native:$LD_LIBRARY_PATH
export SPARK_MASTER_HOST=Master
export SPARK_MASTER_PORT=17077
export SPARK_MASTER_WEBUI_PORT=7078
export SPARK_WORKER_CORES=4
export SPARK_WORKER_MEMORY=60g
export SPARK_EXECUTOR_MEMORY=50g
export SPARK_WORKER_INSTANCE=1
export SPARK_LOCAL_DIRS=/mnt/tmp
</code></pre></div></div>
<p>有的部分,我自己也不是很清楚其背后的含义,总之是参考了多个教程整合所得。容易出问题的地方在于,有时候会跑空内存,就需要在这里改内存。但根据我自己实战的经验,往往内存不足是因为没做到真正的分布式,代码只在主节点执行,直到内存被跑空,需要看yarn配置是否成功。</p>
<p>在所有的一切设置好后,再将spark目录分发到每个机器的相同目录下,即可使用 sbin/start-all.sh 开启整个spark 服务。</p>
<h3 id="服务器配置">服务器配置</h3>
<p>在完成单机的配置之后,只需要分别把同样的内容,分发到不同机器上,再开启服务即可。所以最前面远程调用每个机器、分发文件代码都是保证能高效配置更多机器的基础。一台一台的修改、发送,有可能一天都配置不好集群,但利用批量化的处理,一个小时内即可完成复杂的配置任务,关键是留好副本。</p>
<p>服务器的配置除了分发jdk、scala、hadoop、spark之外,还需要配置免密、修改 etc/hosts 、.bashrc,由于我使用了 Cython 代码,还涉及到远程调用每个机器进行python 包的编译安装,使用 python, 最好将anaconda一起传输配置。</p>
<h3 id="踩坑">踩坑</h3>
<p><strong>坑一</strong>: 机器和集群的问题</p>
<p>除了机器本身访问网络异常等问题外,机器最好在同一个局域网下,这样利用内网IP进行配置和访问,就可以避免防火墙带来的干扰。其次是机器最好预先计算好运行的费用,赶稿期间,机器没钱了,可以是非常致命的打击!此外,最好不要在别人都用的服务器上,做太大规模的计算,频繁地读写,很容易把磁盘写满,也提醒了这类型的分布式,其实更关键的部分在于磁盘大小,而不在于GPU之类的。</p>
<p><strong>坑二</strong>: 设备名与IP</p>
<p>这是我在前两次集群中没有遇到的问题,当第三次在阿里云服务器上配置时,直接让我配置到怀疑人生。明明都是一套路子,但是等到调用Spark时,服务就是起不来。在我检查日志后,反复搜索,都没找到一个解决的方法。事实证明这种bug,可能并不会每次都出现。直到我读到这篇文章<a href="https://zhuanlan.zhihu.com/p/163407531">《spark远程调用的几个坑》</a> 时,恍然大悟,从简短的语句中找到了解决之道。通常在每个终端中的开头都是 <code class="language-plaintext highlighter-rouge">用户名@机器名</code> ,但是如果hosts文件中,没有对应的ip地址,那么在其他服务器中,有可能就无法识别该机器是哪台机器。解决方法正是从这篇文章中找到,在每个服务器的hosts文件中,都加上:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>机器名 IP地址
</code></pre></div></div>
<p>机器名的提取,我也是从每个hosts文件中得到的,再一次体现了,批量收发文件,和批量执行命令的重要性。</p>
<p><strong>坑三</strong>: 漏步骤的执行</p>
<p>即使我配置了那么多遍服务,但在一次又一次的重复配置中发现,我还是可能遗漏步骤。比较常见的是遗漏执行 namenode 初始化,</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bin/hdfs namenode -format
</code></pre></div></div>
<p>遗漏执行spark的启动命令,等等。</p>
<p>相信Spark的配置中,必定仍有坑是我没踩到的,更多的坑也可能出现在 Spark 的执行中,总之道路漫漫。</p>
<h2 id="测试运行">测试运行</h2>
<p>这里给一些简单的测试例子查看配置是否成功。</p>
<h3 id="hdfs-2">HDFS</h3>
<p>创建,查看文件夹;上传文件</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hdfs dfs -mkdir test
hdfs dfs -ls test
hdfs dfs -put test_file test
</code></pre></div></div>
<h3 id="spark-2">Spark</h3>
<p>指定 master 可以启动 Spark 的 不同模式,可以在 Spark 的终端测试 Spark 是否配置成功。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 启动命令
bin/spark-shell --master spark://Master:17077
</code></pre></div></div>
<p>shell 内的测试脚本</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import org.apache.spark.SparkConf
import org.apache.spark.rdd.RDD
val result: RDD[Int] = sc.makeRDD(Array[Int](1,2,3,4,5,6))
result.count()
</code></pre></div></div>
<h3 id="jupyter-notebook">Jupyter notebook</h3>
<p>以下是经过我查找之后,试出来的利用 Jupyter notebook 运行 PySpark 的代码,有的地方可能不太完整,但可以参照类似的设置方式进行修改。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import os
memory = '200g'
pyspark_submit_args = ' --driver-memory ' + memory +' pyspark-shell'+' --num-executors 15 --executor-cores 8'
os.environ["PYSPARK_SUBMIT_ARGS"] = pyspark_submit_args
import findspark
findspark.init()
from pyspark import SparkContext
# 真分布式模式
sc=SparkContext(master="spark://Master:17077",appName="test")
# 本地模式
# sc=SparkContext(appName="test")
sc._conf.set("spark.driver.maxResultSize", '50g')
</code></pre></div></div>
<h2 id="小结">小结</h2>
<p>本文比较粗略记录了配置分布式 Spark 环境,但没有记录详细的 PySpark 代码。根据多次配置的经验,每次更换环境之后,都有可能出现新的问题,但总的来说,网络上都能找到解决方案。我将自己认为比较重要的部分进行了记录,将来如果还需要再基于 Spark 进行研究,只希望能利用此次记录快速再次进行配置。Spark 代码本身的书写并不算特别复杂,虽然我也并没完全掌握,但将来还需要对 Spark 进行更近一步研究时,会考虑再写一篇写 Spark 代码时,遇到的疑难杂症。</p>]]></content><author><name>冥郡</name></author><category term="Python" /><category term="Java" /><category term="other" /><summary type="html"><![CDATA[背景]]></summary></entry><entry><title type="html">如何用 R Markdown 生成每周实验报告</title><link href="/github-io/2020-11-16/R-05" rel="alternate" type="text/html" title="如何用 R Markdown 生成每周实验报告" /><published>2020-11-16T00:00:00+08:00</published><updated>2020-11-16T00:00:00+08:00</updated><id>/github-io/2020-11-16/R-05</id><content type="html" xml:base="/github-io/2020-11-16/R-05"><![CDATA[<h2 id="背景">背景</h2>
<p>本科时,我主要交作业的报告,写成 HTML 文件,再转 PDF ,追求“花里胡哨”,让别人看起来很“认真”、很“高端”的感觉。用 Prettydoc写完全没问题,网页很容易加入特效。但 HTML 打印输出结果时,相信很多人都会遇到打印不完整的情况。转变为研究生和博士之后,有时依然需要每周给导师写报告。这时候,还像交作业一样给导师交花里胡哨的 HTML 就有些没有必要了,这样会掩盖汇报的重点。我们得认识到报告实际上是为最终论文准备的。所有实验最终是需要放到论文上的。学术论文需要图表,图表往往需要直观、体现重点。如果是在赶一篇论文,并需要不断反馈实验进度时,那么就应该考虑是否能比较方便地将实验结果放置到最终的论文中。最终论文需要使用规定的 LaTeX 模板完成,所以每周的实验报告如果都用之前的方式写,必然会需要返工,而且效率也不高。</p>
<p>本科不怎么写 LaTeX,所以当时并没有对 LaTeX 的各种功能深入了解。虽然现在依然是半瓶水,但已经足够将 R Markdown 有机地与 LaTeX 结合起来。本文主要记录最近几周,对 R Markdown 与 LaTeX 结合生成 PDF 的一些经历和理解,本质还是一些搬运工作。虽然我本人并没有开发任何其中用到的任何工具和包,但网络上确实很少有系统地展示将这些功能综合起来能做到什么事的博客。希望能吸引更多人对其进行探索,之后也能方便我自己在未来写实验报告和展示,这是本文的目的。</p>
<h2 id="从-yaml-开始">从 yaml 开始</h2>
<p>yaml是研究任何 R Markdown 模板的开始,研究 yaml 只需查看帮助即可,本文生成的 PDF 使用的引擎主要是 pdf_document ,也就是查看 pdf_document的帮助即可。</p>
<p>它的yaml有几个初学者容易止步的问题,有人使用 pdf_document 生成 PDF时,使用中文就会报错,那完全是对 LaTeX 的中文使用不熟悉导致的,解决 LaTeX 支持中文的方法有很多,我选择最简单,需要耗费最少代码的方式来举例说明。支持中文的要点在于正确使用包+正确使用编译引擎+正确使用编码格式。这里我的组合是 xelatex + ctex + utf8。此时yaml需要写为以下格式:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
output:
pdf_document:
latex_engine: "xelatex"
header-includes:
- \usepackage{ctex}
---
</code></pre></div></div>
<p>此外根据不同的系统,还需要解决可能存在的不同问题,这些对于熟悉 LaTeX 的选手来说都不是什么问题。比如有的mac系统下,会出现找不到中文字体的问题,此时需要把上面引用ctex包的命令改为 \usepackage[fontset=mac]{ctex}。</p>
<p>这里就是使用各种 LaTeX 包写报告的关键,引用包在header-includes 下,按照如上的格式书写。本文希望展示利用 LaTeX + R Markdown,我们到底可以将一个报告做到什么程度。本文展开的思路也就是根据不同 LaTeX 包,去说明不同的功能。</p>
<p>在开始对不同功能展开之前,还有两个值得一改的yaml参数,即keep_tex、keep_md,可以将它们都设置为 TRUE。原因在于,生成 PDF 主要有三个阶段,第一是将 .Rmd 转化为 .md ;第二是将 .md 转化为 .tex;最后才是将 tex 转化为 PDF。保留中间文件在调试和测试时有很关键的作用,可以根据生成的结果去研究到底需要在什么位置对输出结果进行修改。</p>
<h2 id="引用其他方式生成的-pdf">引用其他方式生成的 pdf</h2>
<p>R 语言是很好的胶水语言,直接使用别的方式生成的pdf文件或矢量图,将这点体现得非常到位。实际上,R 在报告中插入图片本质都是在引用chunk生成的 pdf 文件,当生成结束后,中间文件都会删除。使用这个功能需要在yaml中添加</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- \usepackage{pdfpages}
</code></pre></div></div>
<p>引用某个 PDF 只需使用以下的include命令。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>\includepdf[]{ PDF的路径 }
</code></pre></div></div>
<p>看起来这个功能平平无奇,但用的好了,可以将整个报告变得极为完整。例如,在其他设备上手推公式,又不想打字,那只需要转化为 PDF 附加到报告中即可。最近我需要从决策树去看一些统计量的物理意义,发现在python的决策树中可以直接生成树结构的结果,在python中生成 PDF之后 ,我可以很好地解释模型的意义。决策树可以生成结构不会是特例,网上有许多代码,生成网络结构图。特别是生成神经网络结构图,这时候往往都是利用 Python + Graphviz,这也就意味着我们可以很好的利用 R Markdown 将这些结果都整合到报告中。最最最重要的一点在于,这些PDF格式的结果都是矢量图,讲解时可以随意放大。</p>
<h2 id="tikz">tikz</h2>
<p>说到可以利用高级语言生成图片,对于R来说,也可以利用 tikzDevice,这个包可以将R图片转化为tikz的代码。熟悉 LaTeX 也完全可以自己一点点写tikz的代码。要摸清一种数据结构,若不自己实现,感觉都差一点味道。在实现之后,如果能将其可视化,那将帮助别人更好理解。</p>
<p>我在学习跳表时体验了一下这个过程,实现跳表的数据结构很简单,但将其批量绘制为图片花费了我更多的时间。我在给别人讲解时,选择使用 Beamer 生成 slides。在设计完跳表的数据结构之后,我也设计了相应的绘图代码,这里不打算附上源码。</p>
<p>提到tikz最主要的原因在于它是利用 R Markdown 插入 LaTex最好的例子。使用tikz需要在yaml上添加</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- \usepackage{tikz}
</code></pre></div></div>
<p>之后在写tikz代码时,只需要写 <code class="language-plaintext highlighter-rouge">\begin{tikzpicture}</code> 和 <code class="language-plaintext highlighter-rouge">\end{tikzpicture}</code> ,就可以在它们之间插入相应的绘图代码。</p>
<p>作为一个典型例子,在R Markdown中,无论使用任何 LaTeX 代码,都可以用引入包,写引用环境都可以解决。专业或许限制了我的想象,我在公式之外可能用到的环境除了 tikz 以外,还会需要写伪代码,需要的包为 algorithm2e,这里也就不需要赘述了。</p>
<h2 id="流程化">流程化</h2>
<p>到目前为止,主要介绍的都是 R Markdown 引用 LaTeX 的优点。对于熟悉 LaTeX 的人来说,这完全没有必要,换一个编译环境,还是在写一样的代码,节约的代码量也不多,完全没有使用 R Markdown的必要。 对于不熟悉 LaTeX 的人来说,似乎说的这些功能都离得好远,还是使用单纯的 R Markdown比较香。假如我不会R,确实没有必要为了写一份报告学习 R Markdown,这也是它面临的尴尬处境。但我比较希望从下面的例子中,阐明使用 R Markdown + LaTeX 可能达到的化学作用。</p>
<p>假定每几天就要开一次会,每次都可能需要运行新的模型,都要生成实验结果。老板不仅想看趋势图,也想看数值结果,且需要在每个数值结果中都要标注出满足某个条件的数据,例如最好的结果需要标为红色,而且数据集不止一个,最终还需要将实验转化为论文。一份有质量的报告当然不能仅仅只写成Excel,模型的架构图不画出来,又怎么能在最短的时间反应其内在的含义?</p>
<p>综上,实现以上的要求,确实需要几种语言的综合,将其流程化。要想快速运行新的模型,利用 C++ 和 Python 即可,要生成图文并茂的实验结果并方便别人打开查看,使用 R Markdown生成 PDF比较靠谱。需要将实验结果插入论文中时,就只需要保留中间的 tex 文件,倒是提取对应的代码即可。</p>
<h2 id="数据框标注">数据框标注</h2>
<p>将数据以表格的形式展示这一部分是我写这篇文章的主要原因,或许我的解决并非最优方案,但从我解决整个需求的过程中可以看到利用R Markdown + LaTeX 遇到问题时的解决思路。</p>
<p>按照R语言查看数据的习惯来说,一般是将 dataframe 打印出来查看。但随着数据量的增加,我需要标注出数据中满足某些条件的数值。例如论文中,我们需要标注出最优和次优的实验结果。打印 dataframe 的原理是通过 R 将其转变为对齐的数据,再用 verbatim 环境将数据展示,这是通过保留 markdown 文件和 tex 文件看出来的。那么我们要将其上色,则需要寻找查找关于verbatim环境上色的方案。有一个包 <a href="https://www.ctan.org/pkg/fancyvrb">fancyvrb</a> 提供了解决的思路,通过查找它的<a href="https://mirrors.bfsu.edu.cn/CTAN/macros/latex/contrib/fancyvrb/doc/fancyvrb-doc.pdf">文档</a>可以发现,改变verbatim环境颜色并不是很难,只需做到三件事。第一、将 <code class="language-plaintext highlighter-rouge">\begin{verbatim}</code>的环境设置转变为<code class="language-plaintext highlighter-rouge">\begin{Verbatim}</code>。第二、在<code class="language-plaintext highlighter-rouge">\begin{Verbatim}</code>后添加</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[commandchars=\\\{\}]
</code></pre></div></div>
<p>第三、在需要转变颜色的位置插入 <code class="language-plaintext highlighter-rouge">\textcolor{颜色名}{文本}</code> 。这三件事都不太难,比较难的是找到这样的解决方案。经过我的尝试,可以在引用R代码的三个反引号前后,直接写<code class="language-plaintext highlighter-rouge">\begin{Verbatim}</code>就可以让 pandoc 转换代码时不将三个反引号转变为<code class="language-plaintext highlighter-rouge">\begin{verbatim}</code>。第二件事和第一件事是同一件事,只需要在<code class="language-plaintext highlighter-rouge">\begin{Verbatim}</code> 后将<code class="language-plaintext highlighter-rouge">[commandchars=\\\{\}]</code>加上即可。</p>
<p>举个例子,最终代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>\begin{Verbatim}[commandchars=\\\{\}]
```{r echo=FALSE,results='asis',comment=''}
data(iris)
iris$Species<-paste0("\textcolor{red}{",iris$Species,"}")
colnames(iris)[5]<-"\textcolor{black} Species"
print(head(iris))
```
\end{Verbatim}
</code></pre></div></div>
<p>感兴趣的朋友可以尝试一下,大致能满足需求,但解决的并不算完美。由于print的局限性,会自己填充一些空格,使得列对齐,也就意味着如果增加一行<code class="language-plaintext highlighter-rouge">\textcolor{red}{}</code>这样的代码,就会使得这一列变长。要使得整体不被拉长,就需要对这列的所有元素都插入差不多长的变色代码。也就是为什么第五行还需要将列名设置为黑色。</p>
<p>这里还有两点小细节是我在探索过程中,寻找材料时才发现我过去都没意识到的控制指令。在 R Markdown 中可以通过控制chunk做到省略一些不重要的输出。<code class="language-plaintext highlighter-rouge">result='asis'</code>时,可以控制生成的 Markdown代码不产生三个反引号。 pandoc在处理Markdown文件时,遇到反引号且前面没有环境控制时,应该会自动将其转化为verbatim环境。但在我们开始尝试深入结合 R Markdown 的代码和 LaTeX 时,反引号则会累赘。<code class="language-plaintext highlighter-rouge">comment=''</code> 则是将dataframe前面的双井号给替代掉。页宽不够时,不妨将其删除。</p>
<p>研究到这里,在数据框上标注数据已经不是一件困难的事,虽然我还没有解决数据对齐的问题。虽然最终的代码确实简单,但这个过程绝不是一蹴而就的。在没发现在反引号前加引用包的环境时,会替换原有的verbatim环境前,我的方案甚至是保留生成的.tex文件,然后改环境代码,再去LaTeX环境中编译生成。</p>
<p>当然由于直接打印数据框最后的问题实在找不到方案解决,不能止步于将就。多亏发现了一个包<a href="http://xtable.r-forge.r-project.org/">xtable</a>,改变的方案就是将数据框转变为LaTeX表格。</p>
<h2 id="表格标注">表格标注</h2>
<p>虽然不太了解开发xtable作者初心是什么样子的,我猜测或许起初目的只是为了方便将结果转化为LaTeX代码,然后粘贴到.tex文件中写论文而已,但还是要谢谢他。我应该是没有精力和时间去弄一个这样的包,只是为了写报告。为了方便读者,我直接给出解决方案:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>```{r echo=FALSE,results='asis',comment=''}
writeLines("```{=latex}\n")
library(xtable)
data(iris)
iris$Species<-paste0("\\color{red}{",iris$Species,"}")
for(i in 1:10){
print(xtable(head(iris),caption ="iris"),sanitize.text.function = identity)
}
writeLines("\\clearpage\n```")
```
</code></pre></div></div>
<p>假使你仅是想使用我摸索出的结果,对其背后的原理并不想了解的话,那么可以直接套上面的框架即可实现需求。下面将对这段代码给出我自己的理解:</p>
<p>如果需要批量输出,直接使用xtable是不可行的,还是需要print,否则for循环中的内容不会输出到文件中。xtable函数返回的其实是一个变量实例,print则会调用它的方法,将其转变为特定格式的输出。LaTeX改变字体颜色的代码不需要多讲,同时也可以改变格子背景颜色等等,这些都完全属于单纯 LaTeX 和 R 的问题。读者不妨尝试将 <code class="language-plaintext highlighter-rouge">writeLines("```{=latex}\n")</code> 、<code class="language-plaintext highlighter-rouge">sanitize.text.function = identity</code> 等代码删除以后,仅保留for循环和<code class="language-plaintext highlighter-rouge">print(xtable(head(iris)</code>看看会出现什么结果。那么不仅不会变色,而且还会输出许多看不明白的错误信息。</p>
<p>首先我们需要解决的是产生奇怪注释的问题。还是要回到文章的一开头,我们需要保留 .md 文件和 .tex 文件,才能定位错误。在 .md 文件中,一切如常,而在 .md 转化为 .tex 文件时,pandoc将 % 看成了普通字符,在转化为 LaTeX 的过程中,加了转义符号,将其保留。这也导致了注释后的字符也显现了出来。在我又反复查看xtable的源码之后,发现作者挺nice,为我留下了后门。一种解决思路时,加一条全局控制命令<code class="language-plaintext highlighter-rouge">options(xtable.comment=F)</code>,它则不会输出注释。就在我洋洋自喜时,pandoc又给了我一棒槌。在我自己的实验报告中发现(给的例子不会出现这个bug),当输入的表比较多的时候,pandoc会将一些<code class="language-plaintext highlighter-rouge">\begin{table}</code>的命令转变为<code class="language-plaintext highlighter-rouge">\\begin\{table\}</code>。也就是说R代码中无法完全解决这个问题,好在pandoc中也给我留了后门,只要在<code class="language-plaintext highlighter-rouge">```{=latex}</code> 中的代码,它就会直接将其转化为LaTeX 代码。在我调试的过程中,想在print中输出换行符,发现做不到,于是又发现了一个宝藏命令<code class="language-plaintext highlighter-rouge">writeLine("\n")</code>。这个问题就一下子迎刃而解了。也就在上面代码的开头中,从md文件中多输出一行<code class="language-plaintext highlighter-rouge">```{=latex}</code>,结尾再增加上它的结束符。</p>
<p>其次是解决颜色的问题。这也是在反复阅读 xtable 的帮助和源代码时发现的。首先是发现了print中有一个属性<code class="language-plaintext highlighter-rouge">sanitize.text.function = function(x){x}</code> 这看起来可以给所有元素加判断和改颜色。通过对这个属性的搜索,终于在stack overflow 中找到了上面的解决方案。<code class="language-plaintext highlighter-rouge">sanitize.text.function = identity</code> 这应该是xtable作者留下的又一个后门,即当添加了这个属性后,它不会将表中的元素修饰为LaTeX的形式输出,而是保留原始的形态。</p>
<p>最后解释一下<code class="language-plaintext highlighter-rouge">writeLines("\\clearpage\n```")</code>。后半部分的反引号是为了响应前面的<code class="language-plaintext highlighter-rouge">```{=latex}</code>,而<code class="language-plaintext highlighter-rouge">\clearpage</code>则是当表特别多时,LaTeX无法支持超过一定数量的浮动元素,所以加一个clearpage则可以避免这个错误,或是增加一些文字。</p>
<p>值得注意的是,在利用print往文件输出时,需要注意反斜杠有时需要转义。相信书写时,稍微注意不会是什么大问题。</p>
<h2 id="beamer">Beamer</h2>
<p>使用 PPT 做展示报告其实也可以,单纯使用 LaTeX 写 Beamer 也很正常,但鲜有人会考虑使用 R Markdown。我认为它非常具有潜力,但可惜愿意探索的人太少,导致可以参考的资料会比较少。接下来,我将尝试将我的探索和理解写下来,希望有人能循着这个路线,写更多的教程。</p>
<p>在知晓利用 R Markdown 结合 LaTeX 做报告面临种种困境的解决之道后,那么写一个精彩的slides也不会是什么问题。大部分 R Markdown的语法都要么是R的语法,要么是Markdown的语法,少部分人会插入一些LaTeX的语法。</p>
<p>首先beamer的yaml相信看过我之前文章的都比较清楚,例如:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>output:
beamer_presentation:
latex_engine: "xelatex"
theme: "Berlin"
colortheme: "beaver"
</code></pre></div></div>
<p>要查看不同的beamer主题,只需要搜 beamer theme matrix 就可以找到不同组合的效果。</p>
<p>值得一提的是如何控制每页slides。在Beamer中,有些属性在单页调整可以不受干扰。虽然可以在yaml中利用fontsize定义整体的字号。但有些特殊页太挤时,不可避免需要用单页的字体大小控制。此时命令为<code class="language-plaintext highlighter-rouge">\fontsize{10pt}{1pt}\selectfont</code>这里的10pt是字体大小,1pt为行间距的大小,不加selectfont时,有些公式不会一起改变。</p>
<p>一般的slides可以分两页展示内容,所以这里给出一个“左手画圆,右手画方”的例子,以展示R Markdown的优越性。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>```{=latex}
\begin{figure}
\begin{minipage}[htbp]{.4\textwidth}
\centering
```
```{r echo=FALSE, fig.height=7, fig.width=6, result="asis"}
curve(sqrt(1-x^2),xlim=c(-1,1),ylim=c(-1,1),xaxt = "n", yaxt = "n",xlab="",ylab="")
curve(-sqrt(1-x^2),xlim=c(-1,1),add=TRUE)
```
```{=latex}
\end{minipage}
\hfill
\begin{minipage}{.2\textwidth}
\usetikzlibrary{fit}
\small
\begin{tikzpicture}[cube1/.style = {rectangle,draw=red!50,fill=red!20,
inner sep=0pt, minimum height=5cm, minimum width=5cm},scale=.9]
\node at(0, 0)[cube1]{};
\end{tikzpicture}
\end{minipage}
\quad\quad\quad\quad\quad\quad\quad\quad
\end{figure}
```
</code></pre></div></div>
<p>“左手画圆”指的是可以使用R语言绘制特定的统计图形。“右手画方”则指的是利用LaTeX绘制tikz包下的模型示意图。利用的主要是 minipage的环境,利用minipage也可以单独为每个图片加caption。当然,每个minipage中可以加的内容并不限于图片,还可以是文本、公式等。有时候需要将输入的 LaTeX 代码用环境框起来,避免被 Markdown 找不到上下匹配,而被视作需要转义的字符。<code class="language-plaintext highlighter-rouge">\hfill</code> 的作用是使的两侧内容尽可能分开,而<code class="language-plaintext highlighter-rouge">\quad</code>则是一个“推进器”,可以将不太正的图片推到中心,这些都是可以自己调整的。</p>
<p>最后,可以附加一些定制的主题。例如套用一下五年前别人写的 RUC 的<a href="https://github.com/andelf/ruc-beamer-template">模板</a>。使用比较简单,将ruc.sty、png图片等文件,都放在RMD的文件目录下。引用时,加入yaml的包引用即可:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- \usepackage{ruc}
</code></pre></div></div>
<p>我还能想到挺多改变它风格和样式的可能,但并不打算再耗费时间尝试,希望可以有更多人去开发其他有意思的功能。例如现在RUC的背景图片是png,如果存成矢量图,再重新绘图,可以将其改为渐变log。</p>
<h2 id="总结">总结</h2>
<p>本文给出了一些 R Markdown 和 LaTeX 结合生成报告的,主要解决了表格批量插入的问题。我相信在这个基础之上,用 R Markdown 结合更多 LaTeX 其他包相信还能做到更多惊人的事。此外,在 R Markdown 中,插入一些python的代码也并不是什么难事,无非就是对数据流的控制。以上的功能应该也完全能通过内嵌python代码实现。</p>
<p>就我整个探索过程而言,不难发现,现有的 R 包可能已经有了许许多多的功能与环境。比较可惜的是,他们资料并不多,使用的人也并不多,这就导致探索中所能参考的资料比较少。因此,我写此文也是希望能有更多有精力的人,尝试书写更多有意思的报告和展示。</p>]]></content><author><name>冥郡</name></author><category term="R" /><category term="Stats" /><summary type="html"><![CDATA[背景]]></summary></entry><entry><title type="html">2019-2020 领悟的优化 基于c++</title><link href="/github-io/2020-09-02/C-02_Opt" rel="alternate" type="text/html" title="2019-2020 领悟的优化 基于c++" /><published>2020-09-02T00:00:00+08:00</published><updated>2020-09-02T00:00:00+08:00</updated><id>/github-io/2020-09-02/C-02_Opt</id><content type="html" xml:base="/github-io/2020-09-02/C-02_Opt"><![CDATA[<h2 id="背景">背景</h2>
<p>这两年来,主要精力集中在使用c++做矩阵计算上,由此总结了一些c++的优化手段,虽然可能几年以后会对现在的水平嗤之以鼻,但至少可以记录一下自己的编程水平增长经历,以下希望随时间持续更新。</p>
<p>所谓代码的优化,个人认为有三个方面:更快,更省,更好看。快指的是时间少,省指的是省空间,好看指代码简洁。这三者有时候会有冲突,而我所追求的则是达到三者的平衡,有时甚至可以兼顾三者,个人的水平毕竟是有限的。对于尚未工作的我来说,更深层次的优化其实掌握得并不多,目前使用的优化,或许也仅限于单机以及平日研究所用。</p>
<h2 id="底层优化">底层优化</h2>
<p>底层优化是我掌握得比较浅薄的方法。其核心在于利用计算机的金字塔物理结构,提高运算效率。CPU运算速度非常快,但数据在外存,也就是磁盘上,而计算通常都是发生在CPU,一个程序,分为计算密集型和io密集型,我通常面对的任务都是计算密集型,所以重点在于充分利用CPU。这里可以存在的优化有:</p>
<h3 id="读写文件优化">读写文件优化</h3>
<p>通常网上教读写文件的方式是利用fstream,将文件转化为数据流,之后再按照数据类型,一个个地读入和转化数据,这里的优化就可以利用内存和缓存,先将所有的数据读入到内存,之后再进行数据的转换。两种代码如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int n;
ifstream infile(path);
infile>>n;
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int n;
ifstream fin(path,std::ios::binary);
std::vector<char> buff=vector<char>(fin.seekg(0,ios::end).tellg());
fin.seekg(0,ios::beg).read(&buff[0],static_cast<std::streamsize>(buff.size()));
fin.close();
stringstream infile;
std::copy(buff.begin(),buff.end(),std::ostream_iterator<char>(infile));
infile>>n;
std::vector<char>().swap(buff);//释放内存
</code></pre></div></div>
<h3 id="减少cache-miss">减少cache miss</h3>
<p>减少cache miss,其核心想法就是,当CPU在计算的时候,需要的变量都在内存里。这件事其实对人脑也适用,例如大部分人其实都不太能一心两用,更不必说反复切换。如果编写了一分钟c++代码,又迅速切换到python,再迅速切换到java,这样有可能会造成用法的混乱,但是要是一个月都在编写c++,那么这个月写c++都是比较通畅的,再切换python,虽然要几天适应,但不用太久又能很熟练地使用。</p>
<h4 id="图的存储">图的存储</h4>
<p>在数据结构课程中,通常说图有两种存储方式,邻接矩阵和邻接表,邻接矩阵基本不用,在c++上,大部分人都是自己定义图的存法,但在python上,使用的有coo matrix,csr matrix,他们本质上都不是“链表式”邻接表,而是数组式的存储。链表的问题就在于,分配元素时,所占的空间并不连续。当将变量放入cache时,通常都是按块往里放。例如需要读取某个图节点的所有邻居,假使用csr、coo的存法,一口气就可以把好几个邻接的坐标读进去,因为他们连续分配,但对于链式存储,那将可能每次只能读进去几个邻居,由此必然导致新的io,导致速度太慢。</p>
<h4 id="矩阵存储与计算">矩阵存储与计算</h4>
<p>众所周知,矩阵其实本质是数组。所以矩阵可以分为按行存储和按列存储,在c++里是可以自己规定的,其他很多语言是不能自己规定的。 A * B 的速度其实不一定比(B’* A’)’要更快。取决于是按行存还是按列存。根本原因就在于cache,矩阵不能整个放入cache时,必然会一块块往里取,不会跳取,导致计算矩阵乘法时,若两个变量不都在内存时,会导致cache miss,所以提前知道矩阵的存法,并设计乘法顺序是挺有必要的。</p>
<h4 id="空间分配">空间分配</h4>
<p>虽然图用csr存很方便,但是毕竟不是链式邻接表,它插入元素有一个缺点,插入元素时,它无法一直保持原来的结构。所以构造图时,通常都是先生成coo 三元组(u,v,e),再插入图中。例如我最常用的Eigen库,若在构造了Sparse Matrix之后,再插入元素,所消耗的时间有时甚至可能比重新构造三元组再插入的要多,这和哈希表的构造有异曲同工之妙,哈希表通常会多预留一倍的空间,当插入的元素太多时,则批量重构哈希表,虽然这里的重构原因不一定一样,但这不就是编程和人生的常态吗,随着一个系统累积的毛病越来越多时,加补丁吃药亡羊补牢都是于事无补,最终还是看有没有将一切推倒重来的勇气,往往最后才能获得涅槃重生。</p>
<h4 id="引用与指针">引用与指针</h4>
<p>在c++这种“优秀”的语言下,最“好用”的技能当属指针了。其实我是特别不喜欢用指针的,但我比较喜欢用引用。指针经常会出现莫名其妙的错误,犯这些错误通常在于,搞不清楚什么时候分配,什么时候回收,不知道写中止符,造成内存泄露和非法访问。但也只有用好指针才能勉强算是会c++,别的不说,我最常使用的是用指针传递数据。在没有指针的语言里,很多时候“=”都代表复制构造,会将所有值都进行复制,这样带来的是大量时间开销,弄不清指针时,最好的办法是使用引用,传递地址,而不是新建变量浪费空间。虽然这件事也不是绝对的,当需要备份或对两个变量进行操作时,还是只能复制。</p>
<h2 id="利用别人的包优化">利用别人的包优化</h2>
<p>人力有尽时,大部分情况下,别人写的包都是优于自己随手写的代码的,除非算法不一样,否则按照底层优化来说,包通常都会尽可能优化的,只需要注意是否开源,然后用即可。读书人的事,叫偷吗?程序员抄代码叫代码的复用?有人用我写的代码我会非常开心,但未经我同意抄我文章,我倒是会非常不爽。以下将谈谈我对一些矩阵运算库的理解,这里倒是没有做过对比实验,完全基于我自己的感觉,同一个库,不同人用的优化层次是不太一样的,还是根据需求选择比较好。</p>
<h3 id="矩阵运算库">矩阵运算库</h3>
<h4 id="armadillo">Armadillo</h4>
<p><a href="http://arma.sourceforge.net/">Armadillo</a> 算是我使用的第一个c++矩阵计算库,优点胜在语法简单,接近 Matlab,速度其实也还好,关键看用了什么blas。其实这些开源的矩阵运算库,可能是由于经费的原因,代码还是比较朴素的,优化的层次比较低,实际上还是在调用各种矩阵运算,只是一个壳而已。而这些壳可以套不同的线性代数库 BLAS(basic linear algebra subroutine) ,BLAS是一种接口的标准,而不是某种具体实现,具体实现在不同版本下带来巨大的速度差异,CPU下性能最好的,个人感觉是 Intel 开发的 MKL,简直是将 CPU 利用到了极致。这也就意味着Armadillo可以用MKL,也可以用各种 BLAS,从这个角度来说,使用GPU也是可以的,所以水平够的话,何必额外套层壳呢?直接用底层的库不香吗?</p>
<h4 id="mklintel-math-kernel-library">MKL(Intel Math Kernel Library)</h4>
<p><a href="https://software.intel.com/content/www/us/en/develop/tools/math-kernel-library.html">MKL</a>,Intel开发的矩阵运算库,用起来是真的香,速度也是真的快,但是要用得好,就必须好好看<a href="https://software.intel.com/content/www/us/en/develop/articles/intel-math-kernel-library-documentation.html">文档</a>。在国内资料少的情况下,学和用也是真的难,但更难的还是配置MKL环境。以学生身份注册可以有一年免费使用完整版的机会,但这样配置起来还挺麻烦的,踩了不少坑,这里也就不说了,后来过期了是真的难受,不得不用了简化版,最核心的是 Intel 的编译器 icc 用不了,有同学知道除了充钱外,怎么能继续使用,欢迎加我微信联系我,一起探讨这个令人又爱又恨的运算库。</p>
<p>MKL为什么快,可以从CPU的使用情况感受到,但是其内部做了什么优化,并不是如今我的level所能感受到的,但重要吗?重要的其实只在于用就好,就像 python 众多包一样,没多少人会去看底层的代码,好用就完事了,但 MKL 实际并不那么好用,因为函数传递的往往都是指针数组,这就回到了为什么入门最好用Armadillo的原因,MKL快是真的快,但写完程序全是 bug,那能力不行的就直接劝退了不是?</p>
<p>MKL其实还有一个我未能解决的问题,有朝一日解决后,会再写一篇吐槽文章。</p>
<h4 id="eigen">Eigen</h4>
<p><a href="http://eigen.tuxfamily.org/">Eigen</a> 常有博客将这三者比较,但个人感觉三个都可以用,看个人习惯。Eigen和Armadillo一样,都是可以借用MKL加速优化的,甚至由于开源,我也尝试过,直接改 Eigen 源码搭载MKL的接口,速度可以提升不少。Eigen不够快时可以从CPU的使用情况看出原因,凡MKL一运行,大部分时候,所有CPU的核都会用上,但Eigen通常不会。</p>
<p>我个人通常在 R语言里使用c++时,会优先考虑Armadillo,在python 中使用c++时会考虑Eigen,因为有对应的库。但是R里也有RcppEigen,python 也能用Armadillo。说到底还是一个习惯问题,人往往有先入为主的概念,当阅读的论文和代码都是用某一种库时,自然而然的就会用那个库,毕竟改起来复现实验容易,复现代码很多时候并不是那么简单的事。</p>
<p>Eigen 在不同人的手里也会导致很大的速度区别,这些优化Eigen的手段可以在知乎上搜到,最常见的可以有,当生成变量不为输入变量时,可以将普通乘法用以下命令,加快速度:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>M.noalias()=A*B;
</code></pre></div></div>
<h3 id="随机数生成">随机数生成</h3>
<p>这也是看别人论文+代码学会的,自己生成随机数的算法:<a href="http://xoroshiro.di.unimi.it/#shootout">http://xoroshiro.di.unimi.it/#shootout</a></p>
<p>复现别人代码的过程也是学习的过程,吃透越多代码,消化越多,最终转化为的是个人能力,本文的大部分内容,也是这一两年来,我从这些浩瀚的文章代码中提炼出的优化手段。但部分代码至今都还未能消化,这也是水平所限。</p>
<h2 id="编译优化">编译优化</h2>
<p>编译的命令会很大程度影响速度,最常见的例子是,在c++编译时加上 -O3,将会对速度有很大提升,这份提升是编译带来的,可想而知,MKL不能用icc编译,必然效果也会差一些。下面是一些编译上优化的例子。</p>
<h4 id="eigen的附加参数">Eigen的附加参数</h4>
<p>用上这两个吧,我其实也不知道为什么,反正会变快。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> -mavx -mfma
</code></pre></div></div>
<h4 id="armadillo-1">Armadillo</h4>
<p>可以通过改变编译参数,使用不同的blas,这点可以看一下文档。使用不同blas命令不同,而且需要引入不同的路径,其实还挺麻烦的。</p>
<h4 id="mkl">MKL</h4>
<p>不能用完整版MKL时,需要用特殊的g++编译命令,引入路径,非常复杂,而且没有很明确的编译命令,甚至在linux和mac上不一致,顺序也会影响,在我多次尝试之后,命令如下:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mac
g++ main.cpp -std=c++11 -I/opt/intel/mkl/include -I/opt/intel/mkl/lib/intel64 -L/opt/intel/mkl/lib -lmkl_intel_lp64 -lmkl_intel_thread -lmkl_core -I /opt/intel/compilers_and_libraries_2019.3.199/mac/mkl/include -L /opt/intel/compilers_and_libraries_2019.3.199/mac/mkl/lib -L /opt/intel/compilers_and_libraries_2019/mac/lib
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>linux ubuntu
clang++ main.cpp -std=c++11 -I/opt/intel/mkl/include -I/opt/intel/mkl/lib/intel64 -I/opt/intel/lib/intel64 -lmkl_intel_lp64 -lmkl_core -lmkl_intel_thread -L/opt/intel/mkl/lib -I /opt/intel/compilers_and_libraries/linux/mkl/include -L /opt/intel/compilers_and_libraries/linux/mkl/lib -L /opt/intel/compilers_and_libraries/linux/lib -liomp5 -lpthread
</code></pre></div></div>
<p>以上参数有可能有一些冗余,而且需要调整,不使用于每个电脑和服务器,这也是我平常非常不喜欢用MKL的原因之一,实在是太麻烦,太恶心人了,换个服务器就需要再不断地尝试,十分绝望,好在还有下一招。</p>
<h4 id="cmake">CMake</h4>
<p>直接写CMakeLists.txt,然后使用find的方法,找到mkl,并加上编译参数,基本可以换环境后,还能运行。使用cmake还可能会提升一定的运行速度,当基本项目框架一致时,熟练使用CMakeLists可以很好完成代码的升级和迭代(就是不断往里面套用新的库)</p>
<h2 id="并行优化">并行优化</h2>
<p>我最不擅长的一部分,也是最想掌握的部分。</p>
<h3 id="openmp">openmp</h3>
<p>使用openmp优化,其实就是加一条引用omp的参数,再往c++代码中加一行并行的代码,网上资料比较多,这里并没有必要详细给例子。</p>
<h3 id="thread">thread</h3>
<p>使用c++的多线程库thread,难点可能在于加锁等问题上,而我也会皮毛,也就把一个for循环,按照核的多少进行拆分。之后是一个可以深入学习的部分,再加入此文章中。</p>
<h3 id="mkl--cuda">MKL & CUDA</h3>
<p>MKL确实是能利用多核CPU并行最佳的方法之一。CUDA则是使用GPU。</p>
<p>以上,并行提速能做的事很多,但还不是我如今的境界所能掌控的,未来需要提升的还很多。</p>
<h2 id="算法优化">算法优化</h2>
<p>知识就是力量。算法有用吗?视情况而定,重点在于瓶颈在哪。有一种观点是,算法没啥用,差不多就好,堆算法太慢,堆钱快,速度不够快,内存不够大,用钱堆GPU,堆内存,堆磁盘,堆设备就好,但是当一个代码在几十上百核的服务器运行的时间,还没有换一个算法,在普通个人计算机上计算效果要好,速度要快时,我不禁想问,这些钱用在刀刃上了吗?文章开篇所说,时间、空间、代码美观度,三者若要同时达到最佳,那必须要有一个相对最好的算法,这是刨除外物之后,能力的最深层表现。</p>
<h3 id="矩阵结构">矩阵结构</h3>
<p>可以优化的矩阵结构通常两种,稀疏或低秩。也算是我核心研究内容,涉及的算法远不是一篇小文能讲清,知道好用即可。</p>
<h3 id="空间换时间">空间换时间</h3>
<p>不能达到时间空间兼顾时,由于空间便宜,时间宝贵,通常都是采取时间换空间的手段。</p>
<h4 id="alias-method">Alias Method</h4>
<p>Alias Method是对不等权重O(1)时间的采样算法,代价是需要付出构造Alias Table的时间。介绍最清楚的文章当属这篇<a href="https://www.keithschwarz.com/darts-dice-coins/">《Darts, Dice, and Coins: Sampling from a Discrete Distribution》</a> 。对于如此经典算法,我只能说,好用!之前的博文中有写过代码。</p>
<h4 id="sqrt">Sqrt</h4>
<p>求平方根并不是空间换时间的优化方法,这里是一个引子,想说越基础的操作,速度可以是瓶颈。传说有一个神奇的值0x5f3759df,没人知道这个值是从哪里来的,但可以快速计算平方根。我们刚开始编程所熟知的方法是二分查找,实在太慢。连分数法也是一种方法。还可以空间换时间。最后是利用神奇的值的快速开方算法。可以参考博客<a href="https://blog.csdn.net/zmazon/article/details/8217866">开平方的七种算法</a>我也是偶然看到的。</p>
<p>时间换空间固然美,但最优解确是两者兼顾。确实是妙不可言。看起来这样简单的问题没有什么很大提升,但实际复杂的问题往往由简单的问题构成。每一点提升都至关重要,在没有这样最优算法时,往往能做的就只有空间换时间,例如机器学习中的激活函数Sigmoid。</p>
<h4 id="sigmoid">Sigmoid</h4>
<p>Sigmoid函数不算复杂,但如果不从基础函数定义,是否能加速呢?目前我看到的方法就是以空间换时间。由于sigmoid函数自身特点,往外延伸,很快接近0,1,在一定范围之后,直接将值赋值为0,1即可,在此范围内,则可以划分为n个小区间存好,之后求值就成为了单次读取即可得到值的算法。</p>
<p>以上都是非常简单的例子,像这样的基础算法实在太多,例如Partial Sum、桶排序等等。往往我们都只需要将这件事放在心上,看情况去使用即可。</p>
<h2 id="代码优化">代码优化</h2>
<p>多读,多写,多重构,就会感觉到以前写的代码有多垃圾。</p>
<h3 id="c11">c++11</h3>
<h4 id="lambda表达式">lambda表达式</h4>
<p>大部分编程语言都能用,用了就短,短就简洁,缺点是,别人可能看不懂。</p>
<h4 id="auto">auto</h4>
<p>避免迭代变量赋值,当模版套模版时,使用起来其实还挺方便的。</p>
<h3 id="批量操作">批量操作</h3>
<p>以前写R语言时,默认观点时减少写for循环,R不写for循环可以很大程度提高速度,c++则不一定,但至少代码是短的。说起来容易,做起来总是难的,也挺看个人经验。</p>
<h2 id="总结">总结</h2>
<p>本文蜻蜓点水式回顾了2019-2020年我所比较常使用的一些优化技巧。其实每个部分都适合展开长篇大论,限于篇幅和水平,浅尝辄止。代码优化并非一蹴而就,通常在实现一个项目时,我往往会逐步优化,至少让代码先跑起来,再逐渐替换写的不好的部分。这也就意味着,可以积攒以上优化的组件,例如CMakeList、随机数生成、Sigmoid函数计算、Alias Method等。当每次推翻所有代码重构,而不留下一丝一毫时,那就像是在一直生产垃圾,但每次推翻都能积攒下一些组件时,那我认为这样写代码是在挖矿,积攒的代码都是财富。同理,读别人的代码什么都没留下时,又何必浪费时间呢?</p>]]></content><author><name>冥郡</name></author><category term="C" /><category term="algorithm1" /><category term="Article" /><summary type="html"><![CDATA[背景]]></summary></entry><entry><title type="html">论文阅读|MIPS</title><link href="/github-io/2020-08-25/Article-04" rel="alternate" type="text/html" title="论文阅读|MIPS" /><published>2020-08-25T00:00:00+08:00</published><updated>2020-08-25T00:00:00+08:00</updated><id>/github-io/2020-08-25/Article-04</id><content type="html" xml:base="/github-io/2020-08-25/Article-04"><![CDATA[<h2 id="前言">前言</h2>
<p> Maximum Inner Product Search (MIPS) 是我最近感兴趣的一个问题,所以对其做一些调研。MIPS可以将一些稠密的问题转化为稀疏化解决,这在图算法的规模化是很有帮助的,之前的方法有 LSH-MIPS、PCA-MIPS、Diamond sampling approach,2020最新的方法则是Sampling-MIPS,本文将探究这几个算法。</p>
<p>立个Flag, 两个星期搞定。</p>
<h2 id="背景知识">背景知识</h2>
<h3 id="最大点积搜索maximum-inner-product-search">最大点积搜索(Maximum Inner Product Search)</h3>
<p>MIPS的含义正如其名,就是给定一个向量q(query)和一个向量集X(维度必然一致),找出向量集X中与q点积比较大的一些向量。可以表示为:</p>
\[p=\mathop{\arg\max}_{x\in X} x^\top q\]
<p>众所周知,内积大的一对向量,在欧几里得空间下,其物理含义就是它们比较“近”。寻找内积比较大的节点对也就意味着寻找比较近的元素,而寻找相近元素最佳的方法不得不提局部敏感哈希(Locality-Sensitive Hashing)</p>]]></content><author><name>冥郡</name></author><summary type="html"><![CDATA[前言]]></summary></entry><entry><title type="html">论文阅读|图上的自监督学习——对比学习论文解读</title><link href="/github-io/2020-08-14/Article-02" rel="alternate" type="text/html" title="论文阅读|图上的自监督学习——对比学习论文解读" /><published>2020-08-14T00:00:00+08:00</published><updated>2020-08-14T00:00:00+08:00</updated><id>/github-io/2020-08-14/Article-02</id><content type="html" xml:base="/github-io/2020-08-14/Article-02"><![CDATA[<h2 id="前言">前言</h2>
<p> 本文将围绕最近的一些在图上自监督学习的工作,对其中“Contrastive Learning”的内容进行一些解读,并包括一些自监督学习的思路。</p>
<p> 首先,介绍一篇2020的综述《Self-supervised Learning: Generative or Contrastive》,其内容覆盖了CV、NLP、Graph三个方向自监督学习的成果。而本文会将主要目光放在Graph上的自监督学习。</p>
<p> 文章将自监督学习主要分为三类:Generative、Contrastive、Adversarial(Generative-Contrastive)。目前,个人认为大部分Graph研究的目光都集中在Contrstive Learning上。个人拙见,原因可能与图学习的任务有关,图学习的任务主要集中在分类上(节点分类、图分类),对比学习天然会比生成学习更适用于分类任务,所以或许当生成满足某种性质的随机图任务成为主流之后,生成式模型就会成为主流。而对抗式(Adversarial)的学习,则会在生成式学习、对比式学习都达到瓶颈时,得到更好的发展。目前,在图领域,并未看到Adversarial Learning有惊人表现的文章。</p>
<p> 当笔者初识自监督学习时,通过他人的介绍,仅理解为了一种利用自身性质,标注更多标签的一种手段,但随着论文阅读的增加,对自监督本质的理解越来越迷惑。个人理解,其实任意挖掘对象之间联系、探索不同对象共同本质的方法,都或多或少算是自监督学习的思想。原始的监督学习、无监督学习,都被目所能及的一切所约束住,无法泛化,导致任务效果无法提升,正是因为自监督探索的是更本质的联系,而不是表像的结果,所以其效果通常出乎意料的好。自监督学习的前两类方法,其核心想法其实都是想去探索事物的本质。</p>
<p> 本文重点将放在Contrastive Learning的发展脉络上,对于Generative Learning将只结合《Self-supervised Learning: Generative or Contrastive》介绍一些粗浅的理解。</p>
<h2 id="generative-self-supervised-learning">Generative Self-Supervised Learning</h2>
<p>综述中主要介绍了四类基于生成式的自监督模型,最后一类是前三类模型的混合版,而在图学习领域,使用的比较多的应该是第三种,即AE的方法,在后文总结表格中有所体现,这里也就不对混合型生成模型进行描述了。</p>
<h3 id="auto-regressive-ar-model">Auto-Regressive (AR) Model</h3>
<p> 文章提到 “自回归模型可以看作是贝叶斯网络结构”。Auto-Regressive Model 最初是在统计上处理时间序列的方法,时间序列最基础的两种模型就是AR与MA。AR的理论基础确实就是贝叶斯方法,也就是条件概率的一套理论。任意一个节点的分布都可以借其他节点作为条件,以此计算自身的概率分布。这样的思想用在图生成和扩张上,再适合不过。线性回归是最基础的预测模型,预测的结果就是生成的目标。</p>
<h3 id="flow-based-model">Flow-based Model</h3>
<p> flow-based models 是希望估计数据的复杂高维分布。这个方法也可以找到和统计相关的方法。思想其实是广义线性回归模型,都是想用一个潜变量对未知的复杂分布进行估计。</p>
<h3 id="auto-encoding-ae-model">Auto-Encoding (AE) Model</h3>
<p> Auto-Encoder Model 有点像主成分分析方法,其原理是将原有的输入映射到一个新的维度,再将其映射回原来的维度。类似于主成分的方法,这样的操作需要保证映射的目标需要保持某些性质(相似度高的节点,映射后应该相似性依然高),同时这个过程可以降噪。这也是一个非常值得研究的方向。</p>
<h2 id="contrastive-self-supervised-learning">Contrastive Self-Supervised Learning</h2>
<p> 此综述提及,最近因为在自监督学习方向有几项工作有所突破,这些工作都集中在对比学习上,很大程度上说明当前研究的重心主要偏向对比式的自监督学习。这些突破性的工作主要有Deep InfoMax、MoCo、SimCLR。</p>
<p>对比学习最初是想通过Noise Contrastive Estimation(NCE)学习目标对象之间的差别。目标对象之间的区别其实就是相似程度,相似程度是一个比较主观的概念,其实是同任务有关的。通常我们说的挖掘信息,就是在增加衡量相似程度的指标。笔者所接触的最早衡量两个节点相似程度的方法是DeepWalk,从一个节点所能到其他节点的概率,这就是它的相似性。训练这类模型的方法通常有两种,一种是通过定义损失函数,并采样正负例使损失函数最小,另一种方法是直接求解损失函数的极值,通过矩阵分解的方式求最优解。由此,NCE的核心其实是在损失函数,即:</p>
\[\mathcal{L}=\mathbb{E}_{x,x^+,x^k}[-\log(\frac{ e^{f(x)^\top f(x^+) } }{ e^{f(x)^\top f(x^+)}+\sum_{k=1}^K e^{f(x)^\top f(x^k)} } )]\]
<p> 所谓图上的对比学习,其实就是对于任意两个节点,若越相似(属于同一类)其图表示就会越接近,什么样的节点作为正例/负例,就决定了最后分类的效果。</p>
<p> 由于这个损失的分母是比较难计算的,特别是随着负例的增加。之前的方法通常是使用它的等价形式进行训练,即使用Skip-Gram with Negative Sampling(SGNS):</p>
\[\mathcal{L}=\log\sigma(f(x)^\top f(x^+))+k\mathbb{E}_{x^-\sim P_N}[\log\sigma(-f(x)^\top f(x^-))]\]
\[\sigma \ is \ sigmoid \ function\]
<p>而Deep InfoMax 则是在NCE的基础上,走出了另一个道路,其目标为:</p>
\[\underset{\omega_{1}, \omega_{2}, \psi}{\arg \max }\left(\alpha \widehat{\mathcal{I}}_{\omega_{1}, \psi}\left(X ; E_{\psi}(X)\right)+\frac{\beta}{M^{2}} \sum_{i=1}^{M^{2}} \widehat{\mathcal{I}}_{\omega_{2}, \psi}\left(X^{(i)} ; E_{\psi}(X)\right)\right)+{\arg \min _{\psi} \arg \max_{\phi} } \gamma \widehat{\mathcal{D}}_{\phi}\left(\mathbb{V} \| \mathbb{U}_{\psi, \mathbb{P}}\right)\]
<p> 对这个目标函数意义感兴趣的可以直接阅读原文,这里主要关注图学习,所以主要说明它对图上自监督学习的启示。对接Deep InfoMax的工作主要是Deep Graph InfoMax(DGI)。Deep InfoMax主要的启示是利用局部和全局互信息。DGI使用的还是GCN的框架,通过利用readout function得到对节点的一个表示,这里利用了全局信息,再通过构造负例(对应节点的特征重排,再与拓扑结构信息结合),使生成的节点表示更接近正例(对应节点的拓扑结构信息和特征的结合)。由于每次的负例都需要重排特征,这样的生成负例方式是非常耗时的,所以DGI使用了mini batch。</p>
<p> 其实图上许多方法都是从其他领域套用而来,并取得了很多比较好的效果,特别是NLP中的文本就可以看作是一种特殊的图。下面将谈一些基于其他方向,转化到图学习的一些成果。</p>
<h3 id="contrastive-multi-view-representation-learning-on-graphs">Contrastive Multi-View Representation Learning on Graphs</h3>
<p> 《Contrastive Multi-View Representation Learning on Graphs》是一篇ICML2020的文章。他在GCN的三个数据集中都达到了很高的效果。聊这篇论文,可以先分析他的思想来源,弄清思想来源其实是进一步工作的灵感,无中生有的idea其实是非常不容易的,大部分工作还是提出一点改进和迁移现有的工作。</p>
<p> Multi-View其实是在DGI的基础上,对全局和局部互信息进行了新的扩展。他的依据主要是《Learning Representations by Maximizing Mutual Information Across Views》中所提出的对DIM的改进方法:Augmented Multi-scale DIM (AMDIM)。这篇文章提出可以用不同的增强数据的方式,定义局部和全局的互信息损失。在DIM是一个视图生成的“Real”和“Fake”之间的对比,而在AMDIM则是在不同增强视图之间“Real”和“Fake”之间的对比,也就是更好地利用全局信息。</p>
<p> 所谓Multi-View在图像上是各种图片增强的方式,MVRLG则提出将ADJ、PPR、Heat Kernel看作Graph不同的Multi-View。他的核心代码其实就是DGI的代码,区别在于定义了两个GCN,每个GCN对应一种View,衡量正负例的区别时,通过交换正例在不同View下的结果,同交换负例在不同View下的结果,协同训练节点的embedding,同时也可以生成Graph的表示,进行Graph Classification。
以下为Multi-View提出的模型,从左侧开始,定义不同的diffusion(ADJ、PPR、Heat Kernel),并在diffusion上进行采样,并借用DGI的框架,构造两个GNN。通过对特征的重排,生成负例。交换两个GNN的输入,即文章提到的共享MLP,将一个diffusion下的输入作为另一个GNN的输入,再与交换的负例进行对比学习。</p>
<p><img src="/github-io/images/mv.png" alt="The proposed model for contrastive multi-view representation learning on both node and graph levels" /></p>
<p> 这篇文章虽然叫Multi-View,但其实还是在两个View之间进行的实验,论文中有提到,将View增加之后的效果有可能会变差。根据图像的发展思路,在两个View之间进行对比学习必然还是可以扩展的。《Contrastive Multiview Coding》就是认为同一个物体有多种视角(不同的数据增强方式,理论上是无限多的),这篇文章就是希望能够综合多个视角下的信息,对数据进行训练。</p>
<h3 id="gcc-graph-contrastive-coding-for-graph-neural-network-pre-training">GCC: Graph Contrastive Coding for Graph Neural Network Pre-Training</h3>
<p> 在图上一样有 follow CMC的工作,比如同样是唐杰老师组的一篇论文《GCC: Graph Contrastive Coding for Graph Neural Network Pre-Training》。值得一提的是,这篇文章其实也描述了一种图上View的形式,而且与前一篇文章有异曲同工之妙。首先,GCC定义了子图实例,借用Random Walk with Restart 从r-ego网络中,针对某个节点,导出正例子图,并将其他节点通过此方式导出的子图作为负例。</p>
<p> 下图为论文使用的例子,左侧的红色节点为输入节点。以某一节点进行r层的广度搜索所生成的图即为r-ego network。在r-ego network的限制下,从输入节点做RWR,可以生成一系列的子图,这些子图可以作为正例。从其他节点在r-ego network做RWR,生成的一系列子图则可以当作是负例。</p>
<p><img src="/github-io/images/gcc.png" alt="" /></p>
<p> 在《Are Graph Convolutional Networks Fully Exploiting Graph Structure? 》一文中,讨论了RWR与1WL-test有密切的关系。事实上,说两种利用Multi-View做自监督学习的共通之处就在于,RWR本质上就是PPR。原因很简单,每进行深一层的随机游走有一定的概率回到原始节点,而回去的概率是成比例递增的,往下一层走的概率也就成比例递减,并按度均匀分配到邻居节点上,这正是PPR。所以这里导出的子图的分布其实是与节点的PPR分布密切相关的。GCC是一种Pre-Trainning方法,后续使用什么样的GCN Models其实并不是很关键,只需要生成足够多的正负例,类似于图片的训练,输入到GCN中进行训练即可,原文用的是GIN。</p>
<p> 同样的,在图上做自监督学习一样会面临着负例不足的问题,这篇文章利用了自监督学习最新的成果MoCo。MoCo的优点在于不用梯度更新负例的参数,而是用“Momentum“,即用正例参数去更新负例,但需要将这部分的更新控制得非常小。SimCLR和MoCo都是想要解决负例数量的问题,这点倒是与什么领域关系不大,所以利用这些成果是非常自然的事。</p>
<p>对于以上的自监督学习,《Self-supervised Learning: Generative or Contrastive》给出了很好的总结表格,以下摘抄了其中对图自监督学习的部分,并对Multi-View进行了补充,如下表:</p>
<table>
<thead>
<tr>
<th style="text-align: center">Model</th>
<th style="text-align: center">Type</th>
<th style="text-align: center">Generator</th>
<th style="text-align: center">Self-supervision</th>
<th style="text-align: center">Pretext Task</th>
<th style="text-align: center">Hard NS</th>
<th style="text-align: center">Hard PS</th>
<th style="text-align: center">NS strategy</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center">DeepWalk-based</td>
<td style="text-align: center">G</td>
<td style="text-align: center">AE</td>
<td style="text-align: center">Graph Edges</td>
<td style="text-align: center">Link Prediction</td>
<td style="text-align: center">F</td>
<td style="text-align: center">F</td>
<td style="text-align: center">E2E</td>
</tr>
<tr>
<td style="text-align: center">VGAE</td>
<td style="text-align: center">G</td>
<td style="text-align: center">AE</td>
<td style="text-align: center">Graph Edges</td>
<td style="text-align: center">Link Prediction</td>
<td style="text-align: center">F</td>
<td style="text-align: center">F</td>
<td style="text-align: center">E2E</td>
</tr>
<tr>
<td style="text-align: center">DGI</td>
<td style="text-align: center">C</td>
<td style="text-align: center">-</td>
<td style="text-align: center">Context-instance</td>
<td style="text-align: center">MI Max.</td>
<td style="text-align: center">T</td>
<td style="text-align: center">F</td>
<td style="text-align: center">E2E</td>
</tr>
<tr>
<td style="text-align: center">InfoGraph</td>
<td style="text-align: center">C</td>
<td style="text-align: center">-</td>
<td style="text-align: center">Context-instance</td>
<td style="text-align: center">MI Max.</td>
<td style="text-align: center">F</td>
<td style="text-align: center">F</td>
<td style="text-align: center">E2E(batch-wise)</td>
</tr>
<tr>
<td style="text-align: center">$S^2GRL$</td>
<td style="text-align: center">C</td>
<td style="text-align: center">-</td>
<td style="text-align: center">Node attributes</td>
<td style="text-align: center">MI Max. Masked attribute prediction</td>
<td style="text-align: center">F</td>
<td style="text-align: center">F</td>
<td style="text-align: center">E2E</td>
</tr>
<tr>
<td style="text-align: center">GCC</td>
<td style="text-align: center">C</td>
<td style="text-align: center">-</td>
<td style="text-align: center">Context-Context</td>
<td style="text-align: center">instance discrimination</td>
<td style="text-align: center">F</td>
<td style="text-align: center">T</td>
<td style="text-align: center">Momentum</td>
</tr>
<tr>
<td style="text-align: center">ANE</td>
<td style="text-align: center">G-C</td>
<td style="text-align: center">AE</td>
<td style="text-align: center">Graph Edges</td>
<td style="text-align: center">Link Prediction</td>
<td style="text-align: center">-</td>
<td style="text-align: center">-</td>
<td style="text-align: center">-</td>
</tr>
<tr>
<td style="text-align: center">GraphGAN</td>
<td style="text-align: center">G-C</td>
<td style="text-align: center">AE</td>
<td style="text-align: center">Graph Edges</td>
<td style="text-align: center">Link Prediction</td>
<td style="text-align: center">-</td>
<td style="text-align: center">-</td>
<td style="text-align: center">-</td>
</tr>
<tr>
<td style="text-align: center">GraphSGAN</td>
<td style="text-align: center">G-C</td>
<td style="text-align: center">AE</td>
<td style="text-align: center">Graph nodes</td>
<td style="text-align: center">Node Classification</td>
<td style="text-align: center">-</td>
<td style="text-align: center">-</td>
<td style="text-align: center">-</td>
</tr>
<tr>
<td style="text-align: center">MVRLG</td>
<td style="text-align: center">C</td>
<td style="text-align: center">-</td>
<td style="text-align: center">Context-instance</td>
<td style="text-align: center">MI Max.</td>
<td style="text-align: center">T</td>
<td style="text-align: center">F</td>
<td style="text-align: center">E2E</td>
</tr>
</tbody>
</table>
<h2 id="未来可能的工作">未来可能的工作</h2>
<p> 目前在自监督方向可以做的图神经网络学习的内容还是比较多的,MoCo已经用于GCC中,那么在MVRLG中是否能使用呢?而MVRLG在多于两个GCN的效果就会下降,这背后的原因又是什么呢?目前看起来,图自监督学习的大部分结果都是由图像上的理论的提出而推进的,什么数据结构在这个领域其实并不特别重要,但自监督的思想是十分重要的。最后,当前的自监督学习始终还是面临无法将规模做大的问题,例如MVRLG要求每个epoch都将特征打乱,再计算PPR乘特征向量,其实消耗的时间还是挺大的,改进负例的构成尤为必要。</p>
<h2 id="参考文献">参考文献</h2>
<p>[1] Liu, X. , Zhang, F. , Hou, Z. , Wang, Z. , Mian, L. , & Zhang, J. , et al. (2020). Self-supervised learning: generative or contrastive.</p>
<p>[2] Hassani, K. , & Khasahmadi, A. H. . (2020). Contrastive multi-view representation learning on graphs.</p>
<p>[3] Qiu, J. , Chen, Q. , Dong, Y. , Zhang, J. , & Tang, J. . (2020). Gcc: graph contrastive coding for graph neural network pre-training.</p>
<p>[4] Velikovi, P. , Fedus, W. , Hamilton, W. L. , Liò, Pietro, Bengio, Y. , & Hjelm, R. D. . (2018). Deep graph infomax.</p>
<p>[5] Tian, Y. , Krishnan, D. , & Isola, P. . (2019). Contrastive multiview coding.</p>
<p>[6] Hjelm, R. D. , Fedorov, A. , Lavoie-Marchildon, S. , Grewal, K. , Bachman, P. , & Trischler, A. , et al. (2018). Learning deep representations by mutual information estimation and maximization.</p>
<p>[7] P. Bachman, R. D. Hjelm, and W. Buchwalter. Learning represen- tations by maximizing mutual information across views. In NIPS, pages 15509–15519, 2019.</p>]]></content><author><name>冥郡</name></author><category term="Article" /><category term="algorithm1" /><summary type="html"><![CDATA[前言]]></summary></entry><entry><title type="html">人生无常,生死如常</title><link href="/github-io/2020-08-14/Article-03" rel="alternate" type="text/html" title="人生无常,生死如常" /><published>2020-08-14T00:00:00+08:00</published><updated>2020-08-14T00:00:00+08:00</updated><id>/github-io/2020-08-14/Article-03</id><content type="html" xml:base="/github-io/2020-08-14/Article-03"><![CDATA[<p>如星辰般浩瀚的人啊,我走过世间,一生能认识的人或许只是星空的一角。</p>
<p>统计有意思之处就在于,无论整个星空是多么庞大,我们都可以认为,观测到的点点星光,可以反映整个宇宙的所有情况。</p>
<p>而我短暂的人生,所遇到的种种喜怒哀乐,又同别人的有什么两样呢?</p>
<p>人生匆匆数十载,从时间维度上的采样,又如何不能反映一生的起伏?</p>
<p>人生是如此无常啊,意外总是会降临,我所研究的一切,无非是想用数字去量化一些可能而已,但这无常的一切又怎么预测得完呢?</p>
<p>那日,他走了,那些日子,他们走了。大部分人依然是后知者,后觉者,知道又如何?无非是个空空的叹息而已。</p>
<p>将一切量化,生与死都是1,这是不必计算的。我们将能计算的,放在了生命的长度上。为生命的离去而惋惜,并不是因为死去的1,而是难以想象未来所有的可能性,就此终结而已。</p>
<p>人生不是矩阵,人生是张量,大到无法分解的张量。因此人生的拆解也是唯一的。</p>
<p>我站在生与死的中央,看着消失的记忆,原来还有那么一些名字,刻在心上,即使他们的主人已不在。</p>
<p>一年活356天是无常,一天重复356遍是如常。生死放在个人是无常,放在整个人类上是如常。有些人存在世上,需要羁绊;有些人离开了世上,留下了羁绊,其实没有很多奢求,只是希望羁绊能存在得更久一些,虽然不管活着还是死去,羁绊都会在,只是过去绑在两个人之间,如今只会缠绕在自己的心上。</p>
<p>充满随机的世界啊,我即使探索再多的“定数”,也算不出未来,改变不了结果,能改的只有习惯。</p>
<p>是的,随着老去,习惯,生死,如常。</p>]]></content><author><name>冥郡</name></author><category term="Article" /><summary type="html"><![CDATA[如星辰般浩瀚的人啊,我走过世间,一生能认识的人或许只是星空的一角。]]></summary></entry><entry><title type="html">读取Graph数据的代码</title><link href="/github-io/2020-06-28/C-Python-05_read_graph" rel="alternate" type="text/html" title="读取Graph数据的代码" /><published>2020-06-28T00:00:00+08:00</published><updated>2020-06-28T00:00:00+08:00</updated><id>/github-io/2020-06-28/C-Python-05_read_graph</id><content type="html" xml:base="/github-io/2020-06-28/C-Python-05_read_graph"><![CDATA[<h2 id="背景">背景</h2>
<p>记录读图的一些代码,由于图一般都会储存为稀疏矩阵的形式,否则大图根本无法储存,所以最终返回的都是稀疏矩阵,比较节约空间的是csr matrix。</p>
<h2 id="csr-matrix-and-coo-matrix">CSR Matrix and COO Matrix</h2>
<p>COO Matrix 比较容易理解。从图的角度出发其实就是将每条边都存下来。</p>
<p>要想压缩数据,无论什么方法,其实都是在合并同类项。COO Matrix有什么好压缩的呢?单个顶点出发的边可以将它们的顶点合并,用一个数表示。最终也就得到了CSR Matrix。</p>
<p>CSR Matrix可以理解为,先对顶点都预先编号为0-V,那么只需要记录一下,每个顶点有多少出边和出边的位置即可。CSR Matrix由三个数组构成:offsets、edges、values。有时候,offsets 在某些函数输入会拆分为 PointerB 和 PointE,意思也很简单,即某顶点开始时,已经记录边的数量,和此顶点结束时,会记录边的数量。至于 edges 记录的是边的终点,values 记录边的权重。对 CSR Matrix理解的程度决定了,你能对矩阵计算所沉浸的深度,利用CSR进行图的访问和计算是非常方便的,要使有机会写到 MKL 矩阵运算库的解析,会有更深的理解。</p>
<h2 id="python-部分">Python 部分</h2>
<h3 id="mat">Mat</h3>
<p>Mat 格式的文件通常是由 Matlab 生成,读取可以使用scipy.io.loadmat,在图数据中,似乎是约定好的,变量”network”代表图的邻接矩阵,”group”代表的是图结点的分类,所以读取代码为:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 读取图的邻接矩阵
def load_matrix(file,variable_name="network"):
return scipy.io.loadmat(file)[variable_name]
# 读取图的结点的标签
def load_label(file,variable_name="group"):
return scipy.io.loadmat(file)[variable_name]
</code></pre></div></div>
<p>值得注意的是,这里的标签需要根据对应的训练模型调整为合理的形式。例如多分类任务sklearn和logreg应该是有区别的。</p>
<h3 id="edgelist">edgelist</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def load_adjacency_matrix(file):
fl=open(file,'r')
#这里有的-u文件会在第一行放节点个数,则需要用其他方法记录一下节点个数,比如用set
n=int(fl.readline().replace("\n",""))
row=[]
col=[]
while True:
data=fl.readline()
if not data:
break
#这里其实要视数据而定,有时候要加反边,往csr矩阵里导入的时候是不会自动加反边的,否则返回G+G^T
#也许整体读也不错,这里不一定要一行行读
data=data.replace("\n","").split(" ")
row.append(int(data[0]))
row.append(int(data[1]))
data=[1 for i in range(len(row))]
return csc_matrix((data,(row,col)),shape=(n,n))
</code></pre></div></div>
<h3 id="gcn-datasets">GCN datasets</h3>
<p>GCN专用数据有三个,cora,citeseer,pubmed,因为有特征,所以跟其他单纯图数据稍微不太一样。基本都是用tkipf/gcn的读取方式</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def load(dataset_str="cora"):
names = ['x', 'y', 'tx', 'ty', 'allx', 'ally', 'graph']
objects = []
for i in range(len(names)):
with open("data/ind.{}.{}".format(dataset_str, names[i]), 'rb') as f:
if sys.version_info > (3, 0):
objects.append(pkl.load(f, encoding='latin1'))
else:
objects.append(pkl.load(f))
x, y, tx, ty, allx, ally, graph = tuple(objects)
test_idx_reorder = parse_index_file("data/ind.{}.test.index".format(dataset_str))
test_idx_range = np.sort(test_idx_reorder)
if dataset_str == 'citeseer':
# Fix citeseer dataset (there are some isolated nodes in the graph)
# Find isolated nodes, add them as zero-vecs into the right position
test_idx_range_full = range(min(test_idx_reorder), max(test_idx_reorder)+1)
tx_extended = sp.lil_matrix((len(test_idx_range_full), x.shape[1]))
tx_extended[test_idx_range-min(test_idx_range), :] = tx
tx = tx_extended
ty_extended = np.zeros((len(test_idx_range_full), y.shape[1]))
ty_extended[test_idx_range-min(test_idx_range), :] = ty
ty = ty_extended
features = sp.vstack((allx, tx)).tolil()
features[test_idx_reorder, :] = features[test_idx_range, :]
adj = nx.adjacency_matrix(nx.from_dict_of_lists(graph))
labels = np.vstack((ally, ty))
labels[test_idx_reorder, :] = labels[test_idx_range, :]