Hahally's BLOG

- 只想做个无关紧要的副词 -

0%

  auth : hahally

  start : 2020.1.11

后台脚本运行

1
nohup python my.py >> my.log 2>&1 &

colab 长连接脚本

1
2
3
4
5
function ClickConnect(){
console.log("Clicked on connect button");
document.querySelector("colab-connect-button").click()
}
setInterval(ClickConnect,60000)

Django

常用命令 > django-admin startproject locallibrary # 创建项目 > python manage.py startapp catalog # 创建应用 > python manage.py runserver # 启动服务 > python manage.py makemigrations # 数据库迁移 > python manage.py migrate > python manage.py createsuperuser # 创建管理员账号

views.py
    posts.content = markdown.markdown(
    posts.content,
    extensions = [
        # 包含 缩写、表格等常用扩展
        'markdown.extensions.extra',
        # 语法高亮扩展
        'markdown.extensions.codehilite',
        ]
    )

Scrapy 常用命令

          scrapy startproject proj     # 创建项目
          scrapy crawl spider_name     # 运行爬虫

python 第三方库安装源

清华大学镜像
https://pypi.tuna.tsinghua.edu.cn/simple/
阿里云
http://mirrors.aliyun.com/pypi/simple/
中科大镜像
https://pypi.mirrors.ustc.edu.cn/simple/
豆瓣镜像
http://pypi.douban.com/simple/
中科大镜像2
http://pypi.mirrors.ustc.edu.cn/simple/

笔记

      Auth       : hahally
createTime       : 2019.10.26
  abstract       : 大数据辅修学习笔记

jdk环境变量配置

    在、/etc/profile或~/.bashrc中的文件底部
    JAVA_HOME=/usr/java/jdk1.8.0_162
    JRE_HOME=$JAVA_HOME/jre
    CLASS_PATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib
    PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin
    export JAVA_HOME JRE_HOME CLASS_PATH PATH
    [root@master~]# source /etc/profile   使配置生效

windows dos 命令

    C:\Users\ACER>netstat -aon|findstr "8081"      查看端口号
    C:\Users\ACER>taskkill /f /t /im 10144         杀掉进程

Linux命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@master~]# tar -zxvf [*].tar.gz -C [路径]   解压
[root@master~]# yum -y remove firewalld 卸载防火墙
[root@master~]# systemctl stop/status/start firewalld 停止/查看状态/启动/防火墙服务
[root@master~]# netstat -tunlp|grep 端口号 查看端口占用情况
[root@master~]# sudo passwd root 设置root密码
[root@master~]# sudo ln -s /usr/local/jdk1.8.0_162/bin/ bin 创建软链接
[root@master~]# cp [-r] file/filedir filepath 复制文件或目录

ubuntu ens33丢失重连
[root@master~]# sudo service network-manager stop
[root@master~]# sudo rm /var/lib/NetworkManager/NetworkManager.state
[root@master~]# sudo service network-manager start
[root@master~]# sudo gedit /etc/NetworkManager/NetworkManager.conf #(把false改成true)
[root@master~]# sudo service network-manager restart

centos ens33丢失重连
[root@master~]# systemctl stop NetworkManager
[root@master~]# systemctl disable NetworkManager
[root@master~]# sudo ifup ens33 重新连接ens33
[root@master~]# systemctl restart network
[root@master~]# systemctl start NetworkManager

[root@master~]# sudo ps -e |grep ssh 查看ssh服务是否启动

git 命令

1
2
3
4
5
6
git init   初始化
git add filename 将上传文件加到缓冲区
git commit [-m] [注释]
git remote add origin https://github.com/[用户名名]/[仓库名].git
git push -u origin master -f 上传到远程仓库分支
git clone https://github.com/[用户名名]/[仓库名].git 拉取代码

docker命令

1
2
3
4
5
6
7
[root@master~]# sudo docker run -it -v /home/hahally/myimage:/data --name slave2 -h slave2 new_image:newhadoop /bin/bash      运行容器指定共享目录
[root@master~]# sudo docker start slave2 启动容器
[root@master~]# sudo docker exec -i -t s2 /bin/bash 进入容器
[root@master~]# docker commit master new_image:tag 提交容器
[root@master~]# sudo docker rm contianername 删除容器
[root@master~]# sudo docker rmi imagesname 删除镜像
[root@master~]# sudo docker rename name1 name2 重新命名容器

hadoop命令

1
2
3
4
[root@master~]# hadoop dfsadmin -report      命令查看磁盘使用情况
[root@master~]# hadoop jar hadoop-mapreduce-examples-2.7.5.jar wordcount /wordcount/input /wordcount/output 运行jar包
[root@master~]# hadoop dfsadmin -safemode leave 退出安全模式
[root@master~]# hadoop jar x.jar MainClassName[主类名称] [inputPath] [outputPath]

运行hadoop自带MapReduce程序

1
2
3
4
5
[root@master hadoop-2.7.5]# hadoop fs -mkdir -p /wordcount/input              [创建一个目录]
[root@master hadoop-2.7.5]# hadoop fs -put a.txt b.txt /wordcount/input [将文件上传到input文件夹中]
[root@master hadoop-2.7.5]# cd share/hadoop/mapreduce/ [进入程序所在目录]
[root@master mapreduce]# hadoop jar hadoop-mapreduce-examples-2.7.5.jar wordcount /wordcount/input /wordcount/output [运行jar包]
[root@master mapreduce]# hadoop fs -cat /wordcount/output/part-r-00000 [查看输出结果]

Spark

环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vim ~/.bashrc
# 现在我们的环境变量配置看起来像这样
export HADOOP_HOME=/usr/local/hadoop
export SPARK_HOME=/usr/local/spark
export PYTHONPATH=$SPARK_HOME/python:$SPARK_HOME/python/lib/py4j-0.10.7-src.zip:$PYTHONPATH
export PYSPARK_PYTHON=python3
export JAVA_HOME=/usr/local/java/jdk1.8.0_171
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/jre/lib/rt.jar:${JAVA_HOME}/lib/dt.jar:${JAVA_HOME}/lib/tools.jar
export PATH=$PATH:${JAVA_HOME}/bin
export PATH=$PATH:/usr/local/hadoop/bin:/usr/local/hadoop/sbin:/usr/local/spark/bin:/usr/local/spark/sbin:$PATH
export LD_LIBRARY_PATH=$HADOOP_HOME/lib/native:$LD_LIBRARY_PYTHON

# 使配置生效
source ~/.bashrc
spark-env.sh
1
2
3
4
5
6
7
8
cd /usr/local/spark
cp ./conf/spark-env.sh.template ./conf/spark-env.sh
vim ./conf/spark-env.sh
# 在最后一行添加如下配置信息:

export SPARK_DIST_CLASSPATH=$(/usr/local/hadoop/bin/hadoop classpath)
export HADOOP_CONF_DIR=/usr/local/hadoop/ect/hadoop
export YARN_CONF_DIR=/usr/local/hadoop/etc/hadoop
运行
1
2
3
4
/usr/local/spark/bin/run-example SparkPi # 运行例子
/usr/local/spark、bin/run-example SparkPi 2>&1 | grep "Pi is roughly"
/usr/local/spark/bin/spark-submit ../examples/src/main/python/pi.py 2>&1 | grep 'Pi'
/usr/local/spark/bin/spark-submit --master yarn --deploy-mode cluster /usr/local/spark/examples/src/main/python/wordcount.py hdfs://master:9000/words.txt

注意点

运行jar包时,先删掉 /output文件夹,否则无法发查看输出结果

1
2
3
4
5
6
7
8
export JAVA_HOME=/usr/local/jdk1.8.0_162
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export HADOOP_HOME=/usr/local/hadoop-2.7.5
export PATH=$PATH:$JAVA_HOME/bin:$HADOOP_HOME/bin:$HADOOP_HOME/sbin:/usr/local/hbase-1.3.6/bin
export HBASE_HOME=/usr/local/hbase-1.3.6
export HBASE_CLASSPATH=/usr/local/hbase-1.3.6/lib/hbase-common-1.3.6.jar:/usr/local/hbase-1.3.6/lib/
hbase-server-1.3.6.jar

我们终将孤独地走进人群,成为千篇一律的大人。


乌云在城市上空摊开,没有风,没有雨。天气阴沉,空气闷热,世界灰暗,像是电视剧里给回忆片段加了层滤镜。

中午去兴安商业街吃的潮汕牛肉饭,难得合我的胃口,找到位置落座后,我开始大快朵颐。

师姐突然开口说:“你小时候吃饭是不是经常被爸妈夸?”

“没有啊,为什么这么问?”

“因为我看你真的吃的好认真,全程都没有玩手机!我就做不到。”

我咧嘴一笑,低头喝着碗里的热汤,陷入了沉思。

不知道从什么时候开始,吃饭、散步、午休这些稀疏平常的事情变成了难得的享受,我会以最高的礼遇对待它们。

吃饭不紧不慢的,恨不得把每一种调料都看一遍才入口。

午休时总会先听一会歌,找到适宜的姿势才安心睡下,醒来后伸伸懒腰,呆坐在位置上放空好一会。

晚上下班回寝室的路上最舒坦,就那样慢悠悠的走着,看看路灯,数一数楼层里亮着的格子,偶尔遇见猫聚一起开会似的,若是来得及还会去买点夜宵。


这时右手边的位置来了两个小男孩,一大一小,老大可能六七岁,小弟五六岁的样子。

“你们先在这玩会噢,不要乱跑,等妈妈下班带你们出去玩,好吗?”一个女人走了过来,轻柔的声音像从湖面上吹来的微风,吹进两个男孩的耳朵里。

见两人乖巧地点头答应,女人才放心走进旁边的店铺投入到忙碌的工作中。

母亲和员工的身份在那个女人身上切换自如,我不由得心底暗自钦佩她。

兄弟俩把玩着桌上各种各样的玩具,你一句我一句的回应着彼此。

我这才注意到桌子上装着玩具的袋子上写着醒目的生日快乐。

有人生日么?原来他们要等妈妈下班带他们去庆祝生日么?

“你不要那么幼稚了噢,你现在又大了一岁诶。”哥哥把弟弟手中的那个昆虫形状的玩具夺走,然后从袋子里掏出一辆玩具货车给他。

这就是他说的不幼稚么?

“我要去上个厕所,你在这玩哦,不要走开,知道吗?”哥哥嘱咐完就往洗手间奔去。

在这个幼稚的年纪说着不幼稚的话又做着那么幼稚的事,还真是难得哦。

“生日快乐!小朋友。”我突然动了下嘴唇,忍不住说出了这句祝福。

可能是我声音太小,周围太吵,又或者他完全沉浸在获得一辆玩具车的喜悦中,并没有听见我说的话。

不过我有些庆幸他没有听见,哥哥很快就跑了回来,站在了我和他之间,纤细的胳膊挽着弟弟的肩,像世界上最好的盾。

热气扑腾的店铺里,那个女人总会在擦汗时,把目光挪到兄弟俩身上,偶尔和哥哥对视上便嘴角微微上扬,随即又隐入没完没了的白雾中。

这个空间被分成不同的格子,每个格子里有琳琅满目的美食,也有亿万滴的汗水在蒸发。


银白色灯光下,人影交织,眼前的世界好像是被抽帧了一样突然变得卡顿,一些画面从某处被抽出来。

读小学时办过走读,每天中午要走回家吃饭,然后再返回学校上课,但是家离学校却并不近,来回要花上一个小时。

那时候很讨厌走读,我问老妈说回来吃饭会不会迟到?老妈信誓旦旦地说这么近不会迟到,妈骑车接你。于是我成了班里为数不多的走读生。

第一天中午下课后,我在校门口等了很久,久到同学们吃完饭拿着洗得干干净净的瓷碗回教室了。

我很生气,一个人朝回家的路走去。

烈日炎炎,蝉鸣不绝,也是今天这般闷热。

正当我走到一段下坡路时,见到老妈的身影,她推着那辆破旧的自行车爬上坡。

那是一段很陡的坡……是我迄今为止见过最陡的一段路。

老妈擦擦汗,见到我,脸上露出了笑容。

而我皱着眉,怨气冲天。

老妈把我放在后座,继续推着车下坡,倾斜着身体抵抗重力,双手紧紧握着把手,双脚小步地挪动着。

“妈,你真的会骑自行车么?”我开始怀疑老妈根本不会骑车。

“肯定会,骑自行车很简单的,现在下坡不好骑,等下去再说。”

我歪着头,半信半疑,汗水从老妈的脸颊滑过,从下巴落到水泥路上。

到了平地,老妈装模作样准备骑上车,试了几次后都失败了。

我从后座跳下,气呼呼地埋着头,一声不吭地走在她的前面。老妈推着那辆哐哐作响的自行车跟在我后面,同样没有说话。

老妈就是世界上最大的大骗子……

我就是世界上最大的大傻子……

我在心底呐喊,咆哮,宣泄着我的幼稚。

……

大骗子的儿子是大傻子,大傻子的老妈是大骗子……

走着走着,我的思绪被树上的蝉带偏了,脑子里开始出现这些乱七八糟的绕口令。

时至今日,我庆幸那天的蝉鸣盖住了所有的委屈和泪水。

此后的岁月中,老妈用拙劣的骗术不止一次骗过我,而我会尽量配合着她。

比如我不明白老妈为什么总问我墙上的挂钟几点了,后来才知道,她已经有老花眼了。

今年过年,老爸老妈突然提起结婚的事情,我问老妈那这些年你觉得你过得幸福么?

老妈露出笑容来回答当然幸福啊。

我苦笑一声,没有继续说下去。

老妈才是大傻子,那辆跟你齐肩的自行车,你怎么可能骑得了哦。

吃完饭,我汇入人群,成为千篇一律的大人中的一个。


人来来往往,大人的世界井然有序,各司其职,不会因为有一个小男孩生日而有所不同。

这一天,这个小男孩会因为这井然有序的世界而有所不同么?

他今天拥有了一辆货车,明年说不定会有一架飞机哦,他算得上是一个……一个不幼稚的小孩诶。

将来应该会有不少人夸他懂事吧,可他只是一个不那么幼稚的小孩诶。

他们就这样混在大人堆里,有说有笑地享受着时间流逝带来的快乐。


对于我这样的大人来说,此刻时间流逝带来的只有困意,等待我的是短暂的午休,然后是茫茫大海般的论文。

大人也只是时间久一点的小孩,不是么?

大人的心底都住着一个死小孩,不是么?

什么都会过期,大人就是临期食品,不是么?


阴天快乐!!!

月色皎洁,微风徐徐,湖面倒映着对岸的图书馆,还有石桥上散步谈心的情侣。

假期里,寂静的夜,仿佛能听见学校各个角落里的窃窃私语。

啪嗒……


突如其来的高分贝声响把路过的蛤蟆先生吓了好几跳!

“刚刚什么死东西发出的动静?”蛤蟆先生还没有缓过神来,嘴上已经开始骂骂咧咧了:“差点小命就没了,哪个没长眼睛的,不知道看路么?我们蛤蟆的命就不是命么?”

角落里,猫小姐蜷缩着身体一动不动,看起来也被吓得不轻。

“喂,猫小姐,你知道刚刚出什么事了么?”蛤蟆先生抬头看见了那只巨大又胆小的猫。

猫小姐不语,只是一味地把目光望向不远处灯光暗淡的黑影。

蛤蟆先生顺着目光寻过去,那是比猫小姐更加巨大的庞然大物……

“那是……可恶的人类……我最讨厌人类了!你不怕么?猫小姐。”蛤蟆先生脸气呼呼地鼓起来,一副痛心疾首的样子。

“对了,你知道镜湖怎么走么?听说天鹅女士今晚会来对不对?”蛤蟆先生这才想起正事。

“从我后面的台阶下去就是镜湖咯,我不知道天鹅女士会不会来,但是我听人类说癞蛤蟆想吃天鹅肉,就是痴心安想。所以,你还是死了这条心吧。”猫小姐一开口就扎蛤蟆先生的心。

“谢谢你,我走了,你小心点。”蛤蟆先生淡淡开口,语气低落,蹦蹦跳跳地离开了,像泄了气的气球。

“嘿,你去哪?”猫小姐意识到说错了话,想要弥补什么,“要是哪天我看见她了就告诉你,好不好?嘿,小心~”

已经来不及了,那落魄的背影渐行渐远……

“可他们也曾喊过我蟾蜍,喊过我金蟾啊!”他抬起头望了一眼弯弯的月亮,忽然间又想起“玉蟾”,想起“蟾宫”,哦,原来那么久远了么?


我有些失落地坐在地上,随即四处张望,心想应该没有人看见刚刚那一幕吧,假期大家都出去玩了应该没人看见灯光这么暗应该看不清吧。

等等,那是什么?一只小小的身影忽闪而过……

我拍了拍手上的灰,起身前去查看,原来是一只蛤蟆一跳-跳的过马路。

噢,蛤蟆先生五一会放假么?它平常做什么工作?它是出来散步的么?是出来觅食对吧?怎么形单影只的?

我慢慢靠近观察,随即它纵身一跃没入黑乎乎的灌木丛里。

一回头发现台阶旁一只猫趴在那,猫小姐也有心事么?

没等我靠近,猫小姐迈着优雅的步伐走了……

我拾起摔翻的滑板,拿出手机打开手电筒寻找被摔断的珠串。

偌大的广场上,空空荡荡,却装不下蛤蟆先生的幻想,装不下猫小姐的心事。

月色朦胧,苍穹之下,大家的思绪各不相通……

我继续滑行,来回穿梭。上板,滑行,荡板,尾刹,180度转弯,重复练习着这些简单的动作。

光滑的地板上,光影交错,恍惚中有些分不清此刻站在滑板上的人究竟是谁。

是十八九岁的自己么?

是哦,滑滑板这件事本该是那个时候就该做的,我却逃到了二十多岁才有勇气站上去。

哦,原来那么久远了么?

我的动作越来越熟练,滑得越来越快,好似风驰电掣一般,像要追上什么虚无缥缈的东西。

月亮隐入云层,一人坐在广场的台阶上,吹着晚风,旁边是散落的手串和那伤痕累累的滑板。下面就是镜湖,再过去就是华丽的图书馆,再远些是校外的一片小区楼群。


夜晚十一点,我从实验楼出来,悠闲地走在镜湖石桥上忽然回头望了一眼。

远处路灯散发着微弱的冷白色灯光,那片广场像被长镜头越拉越远,远到足足有六七年哦。

仿佛看见一人枕着滑板乘凉,身后还有无数身影踏着滑板起起落落……

微风中断断续续传来一声:嗨,我终于快追上你了!

突然想起白日梦想家里的一句话:无论这里发生过多少腥风血雨,今天的我们也只能闲庭信步了。

岁月流转,风雨飘摇,总有花在错误的季节里盛开。

镜头暂停,定格在这一帧,这是我的25号底片么?


镜湖边上的空地。
准备上板
手串断了
角落里的猫小姐。
逃串的蛤蟆先生。
朦朦胧胧的月色。
下面就是镜湖,对面是图书馆。
石桥上拍的远处小区楼房。

五点半,外面传来窸窸窣窣的声音。

好像有人在翻垃圾桶。是在清理垃圾么?这么难吗?

有人这个点还没睡,有人这个点已经开始为几两碎银奔波了。

我要不要出去看看?刚好床头有两个空的矿泉水瓶。我小心翼翼地下床,去上了个厕所。

外面已经亮了,世界的轮廓清晰可见。阳台正对面是一条宽大的马路,时不时有车疾驰而过。

马路过去是一片精致的小区,高楼林立。在一片铅灰色的背景下,透过薄纱般的雾,那些楼宇仿佛有棱有角的士兵,整齐划一列成方队,威严肃穆。

守护着沉睡的人,困住过往,锁着未来……

翻找的声音还在继续,我犹豫了一下,还是爬上床躺着。我在想那样做会很奇怪吧,对谁来说都不够体面,不是么?

熬夜的人应该假装没听见早起的人的动静,早起的人也应该不知晓有人还没有睡。这样的世界才能正常运转,大家才能过得安安心心,不是么?

我闭上眼假装入睡,试图哄骗大脑自己已经睡过很久很久了。

可那阵声音一直在耳畔回响,像施了魔法的咒语。我可能是魔怔了,辗转反侧,异常清醒。我骗不了自己的大脑。

稍稍拉开床帘,刺眼的光从阳台闯进来,也才六点啊,已经亮到种程度了么?

哦,原来已经是夏天了,太阳直射点往北回归线偏移,北半球昼长夜短。广州市恰好在北回归线附近,夏季的某一天会有一年里最长的白昼和最短的黑夜。

还真是有点期待……

塑料垃圾袋的摩挲声渐渐消失殆尽,随即是电梯门的开合声,是走了么?还会去八楼或者十楼么?

我闭上眼,想象着自己就是那个驮着垃圾袋的人,赶往下一个垃圾桶翻找一些有价值的东西。

可垃圾桶里什么东西有价值呢?塑料瓶?纸壳子?铝合金制的饮料罐?

我得抓紧时间,在那些睡在方格子里的人出门前把整栋楼的垃圾桶翻找一遍,希望今天有所收获。

时间一点点流失,我拖着一大袋子垃圾走进电梯,按下一楼。拿衣袖擦了擦额头的汗,电梯突然停在某一层,我开始有些紧张,往角落挪了挪位置。

一个学生走了进来,穿着运动装,背着羽毛球拍,一整个容光焕发,精神抖擞。

他们已经醒了么?

走出大楼,金色的阳光照得我局促不安,下次我要在早些才是。天亮的越来越早了,人们也起得越来越早。

我继续走着,手里提着的垃圾袋消失了,右肩上挂着帆布袋,走在去实验室的路上。路旁一只小橘猫慵懒地舒展身体,有人骑着车慢悠悠地溜坡,世界正常运转着。

不是说昼长夜短么?为什么还是觉得夜长梦多……

是的,我醒了,看见老师在群里发消息,论文里有个数据好像不对,我不得不跑去实验室打开电脑确认。距离那条消息已经过去一个小时,我没有回复,刚刚又给我发了一句在吗。

我加快步伐,略显狼狈,额头开始冒汗。

为什么这条路这么远?为什么宿舍楼和实验楼要建在学校两头?

突然想起一句很古早的话:身体和灵魂总要有一个在路上吧。

可是,精致的房子锁住了我们的灵魂,窄小的工位捆绑了我们的身体。

时间锈蚀灵魂,欲望操纵肉体,我们走在一条名为万花筒的路上。

2023年6月,印象中长沙的夏天总是闷热,让人透不过气来,今年也不例外。

现在回想起来,仿佛隔了很久很久,像翻开一本历史书中的某一页。

一晃六月就到了,彼时研二即将结束。

那些天,我开始疯狂地投简历找暑假实习,大概中旬的时候进了一家小公司。

入职第一天中午吃饭的时候,大家在互相吐槽。K突然问我:会不会下午就跑了?我犹豫了一下,回答他:先干着吧。

是啊,先干着吧,投了很多份简历都石沉大海,目前这家公司做的事又刚好和我专业对口,再找下去夏天该结束了吧。最重要的原因可能是不想待在学校写论文,和导师之间有着说不清道不明的隔阂,也没有很想要去沟通,我选择逃避,所以跑出来实习了。

不久前,她才找我谈过话,十多分钟下来,我全程敷衍地点头简单回应。我记得窗外蝉鸣很吵很吵,可我并不知道它们在吵什么,否则我应该很乐意加入其中吧。

没过几天,我便发消息告诉她,我找了一个暑期实习。实习之后,我把论文的事暂且抛在了脑后。

在我上班第二天,来了一个女生L,做产品经理实习。至此,这个公司达到人数最多的时候。6个实习生加一个人事和一个会计。人事我们叫王哥,会计我们喊廖姐。两个人坐在最里面靠窗那一排,平常也不说话。

当时我们正以公司的名义参加一个比赛,而赛题与我前不久做过的比赛类似,于是我直接把原来的方案拿过来了,索性取得了不错的成绩,老板脸色也明显好了一点。

上班的时候,我们几个实习生还能时不时嘻嘻哈哈几下,小蜜蜂上也经常发些表情包。K和我一个学校大我一届,那时他经常在小蜜蜂上喊我们休息一下。我左边是后端大佬,不是在debug就是在与环境作斗争。L开始那几天经常被老板喊去打电话。我对面的留学生负责查找比赛相关的资料,我训练模型,K精致的后处理,我们调侃称这是公司的核心科技。

六月下旬,我们就这样还算愉快的过去了,我们也以第三名的成绩进入了复赛。

30号那天,刚好周五。后端大佬已经和老板谈离职了,中午我们一起吃了顿散伙饭,后端大佬说想去外面的地方看看。那天下午快下班的时候,我们被老板喊进办公室开会,拖延了十多分钟,出来时,后端大佬已经先走了。那天下班回学校的路上还挺感伤的,平常都是一起去地铁站,今天突然少了一个人,而且明天也是,心里莫名有些空落落的。

后来,看朋友圈得知,后端大佬去了一趟南宁旅游,再后来去深圳上班,那里应该就是他说的外面的地方。

周末结束,七月第一天上班,那天天气格外的燥热。K一大早就来了,在办公室和老板谈话。印象里谈了很久,我以为他也要走了,突然一下子觉得上班没有一点意思了。最后老板卖惨挽留他,磨了他很久,他才转正继续留下。后面又招了一个产品和一个后端接替之前的工作。新来的后端不怎么说话,平常一个人摸鱼就在那玩纸牌游戏。

某一天突然又觉得生活变得无趣起来。

每天七点多起床洗漱完出门。这个点学校里已经很热闹了,有人在打篮球,有人在打太极,有人在跳舞……在校门口买个早餐,边走边吃,到地铁口差不多八点左右。早高峰确实有点吓人,一号线经常是满的,偶尔有个空位,我站进去刚好塞满。六号线换乘,人来人往,匆匆忙忙的脚步声回荡在站内,都是打工人在赶地铁。

记得来面试那天第一次觉得这个换乘站好大,换线都要五到十分钟。上班后,突然感觉这个站还是小了。有天换乘的时候坐反了,那天迟到了半个小时。出站后,还要走个十分钟左右才到公司。路边很多早餐店,有一个路口围坐一群大爷在那打牌。有家药店门口经常放一把椅子,椅子上栓了一只小泰迪。再往前有所小学,六月份的时候经常有摆摊卖小吃,之后转弯,过个路口,就差不多到公司所在的那栋楼了。到公司坐下,缓一缓,然后开始干活。中午吃完午饭,就趴桌子上睡午觉。下午差不多快两点继续干活。六点下班,K住附近,在路口与我们分开。去地铁路上,只有我,留学生和产品L三个人。我们总结了规律,如果六点下班,正常的话会赶上六点二十二或者二十四的地铁,而且在车头位置大概率能够抢到位置坐。第一个下车的是L,然后是我,最后是留学生。我需要转一号线,人就比较多了,出站后大概七点多,然后在路口等红绿灯,我会找辆共享电动车坐一会,时不时看看天,看看来往的车,看看聚集的人,看看对面的倒计时。晚饭选择并不多,大概率杀猪粉,其次黄焖鸡,偶尔炒饭,记得有段时间吃了一周的杀猪粉。吃完回到自习室休息一下大概就八点了。这段时间还参加了高校大数据挑战赛,所以回学校后,基本在做这个比赛,竞争很激烈,拿奖也比去年难,但是还是想试一下。

日子不紧不慢的过着,直到复赛的日子到来。那天收拾东西准备去南京参赛,一大早老板告诉我们票没有抢到,让我们自己购票。拖着行李出门到高铁站那一段时间有点煎熬。甚至都不想去了,后面疯狂抢票,买到了九点之前的,不过要从武汉换乘。折腾一早上,也总算坐上了高铁。

一段神奇的旅途就这样开始了……

论文:Deep Semantic Text Hashing with Weak Supervision,SIGIR,2018

论文提出一种弱监督学习方法。采用bm25对相似文档进行排序,提取数据中的弱监督信号。先训练一个可以得到整个文档的语义向量表示的模型,然后根据语义向量,运用一些规则(设置阈值)将对应维度变成0或1。

  • 通过使用无监督排序来逼近真实的文档空间,从而弥补了标记数据的不足。
  • 设计了两个深度生成模型来利用文档的内容和估计的邻域来学习语义哈希函数。(NbrReg和NbrReg+doc)

两个语义向量表示模型(NbrReg和NbrReg+doc)区别在于是否利用了近邻文档信息。每个模型包含两个部分:encoder、decoder。

该方法步骤包括三个部分:Document Space Estimation —> NbrReg(NbrReg+doc) —> Binarization

  • Document Space Estimation:得到整个文档数据的空间分布情况

在有标签信息的情况下,可以得到真实文档空间分布。没有标签信息的时候,利用bm25为每个文档 d 检索出一组与之最相似的近邻文档NN(d)。论文假设:近邻文档中大多数与文档 d 具有相同标签,因此任何文档的二进制哈希值在相近的向量空间模型中应该更加近似。

  • NbrReg:语义向量模型

文档语义向量 s ,满足标准正态分布 N(0,1)

wi ∈ d ,概率 PA(wi|s)j ∈ NN(d) ,概率PB(j|s)

定义联合概率: P(d) = ∏iPA(wi|s)P(NN(d)) = ∏jPB(j|s)

目标函数:最大化P(d, NN(d)) = P(d)P(NN(d))
$$ logP(d,NN(d)) = log\int_{s}P(d|s)P(NN(d)|s)P(s)ds\\\geq E_{Q(s|·)}[logP(dd|s)] + E_{Q(s|·)}[logP(NN(d)|s)]-D_{KL}(Q(s|·)||P(s)) $$ 其中 Q(d|·) 表示从数据中学到的近似后验概率分布;· 符号表示输入随机变量的占位符;DKL 表示KL散度;

Decoder Function $$ P(d) = \prod_{i}P_A(w_i|s)=\prod_{i}\frac{exp(s^TAe_i)}{exp(\sum_{j}s^TAe_j)} $$ ej 表示一个词袋向量,矩阵A将语义向量s映射到词编码空间。P(NN(d)) 与上面类似,只是映射矩阵用B表示。

Encoder Function

定义 Q(s|·) 为文档d参数化的正态分布:Q(s|·) = N(s, f(d))f(·) 函数将d表示为均值为μ 标准差为σ 正态分布的向量。 为了表征两个参数,定义f =  < fμ, fσ> ,相当于定义了两个前馈神经网络: $$ f_{\mu}(d) = W_{\mu}·h(d)+b_{\mu} \\f_{\sigma}(d) = W_{\sigma}·h(d)+b_{\sigma}\\h(d) = relu(W_2·relu(W_1·d+b_1)+b_2) $$

语义向量s从Q中采样: s ∼ Q(s|d) = N(s; μ = fμ(d), σ = fσ(d))

  • Utilize Neighbor Documents:(NbrReg+Doc)

论文中提到相邻文档使用的一组单词可以表示该区域所有文档的主题,但是来自相邻文档的额外的词可能会引入噪声,混淆模型。为了削减噪声带来的影响,引入了一层隐藏层,用该层向量来表示近邻文档,使用一个平均池化层得到 近邻文档的中心表示。只有编码器部分有所不同,其他与NbrReg一致。 $$ Z^{NN} = relu(W_2^{NN}·relu(W_1^{NN}·NN(d)+b_1^{NN})+b_2)\\h_{NN}(NN(d)) = mean(Z^{NN})\\f_{\mu}(d,NN(d)) = W_{\mu}·(h(d)+h_{NN}(NN(d)))+b_{\mu} $$

  • Binarization

根据编码器 Q(s|·) 为文档d生成一个连续的语义向量。论文中使用编码器输出的正态分布的均值来表示语义向量 $\overline s = E[Q(s|·)]$,然后使用中值法生成二进制编码。若大于该阈值就令该位为1,否者为0.

思考

论文并没有显示道德直接学习二进制表示,而是通过训练一个语义模型,假设语义相近文档对应二进制表示应该相近,然后通过语义向量进一步转化为二进制哈希值。值得一提的是语义向量是服从正太分布的,一方面便于训练,另一方面也可以给模型提供很好的可解释性,所有文档可以映射到正态分布的语义空间,语义相近的向量具有相近的分布值(论文假设语义向量服从正太分布,并用其均值表示),这也确保了二值化的时候语义相近的文档在映射为二进制哈希值后也保持距离相近。

开源代码

github上找到两处开源代码,一个是作者的低调开源,一个是路人甲的好心复现。

  • 作者开源:https://github.com/unsuthee/SemanticHashingWeakSupervision

  • 复现代码:https://github.com/yfy-/nbrreg

作者开源的代码,一言难尽,虽然很贴心的把对比模型也复现了出来,但是数据没给,如何用bm25算法处理的过程都给省去了。于是找到了一个好心人提供了nbrreg模型的复现,而且给了一份数据,以及对数据进行处理的代码。但是模型训练没有考虑到用gpu的情况。所以下面主要对复现代码进行分析。

数据处理

提供的数据是20newsgroups数据集,20ng-all-stemmed.txt:18820行,20个类别

1
2
alt.atheism	alt atheism faq atheist resourc archiv name atheism resourc alt atheism...
···

格式为:label w1 w2 w3…,一行为一条数据,由标签和对应文档组成,文档由一个空格分开的词组成。

数据处理代码为:prepare_data.py

  • 输入:20ng-all-stemmed.txt中的文本
  • 输出:train_docs、cv_docs、test_docs、train_cats、cv_cats、test_cats、train_knn
    • train_docs、cv_docs、test_docs:分别为训练集、验证集、测试集,维度为vocab_size。
    • train_cats、cv_cats、test_cats:对应标签,one-hot向量,维度为20。
    • train_knn:train_docs中每条数据的近邻文档的索引。

这部分代码主要是得到用于模型输入的数据,即将文本数据用数值表示。这里将每个文档用bm25权重值表示。BM25是信息索引领域用来计算query与文档相似度得分的经典算法。论文中使用bm25检索近邻文档,作为训练的弱监督信号。

BM25的一般公式: $$ Score(Q,d) = \sum_{i=1}^{n}W_i*R(q_i,d) $$ Q表示一个query,qi 表示Q中的单词,d表示某个搜索文档。Wi 表示单词权重,用idf 表示: $$ idf(q_i) = log\frac{N-df_i+0.5}{df_i+0.5} $$ dfi 为包含了qi 的文档个数。依据IDF的作用,对于某个 qi,包含 qi的文档数越多,说明qi重要性越小,或者区分度越低,IDF越小,因此IDF可以用来刻画qi与文档的相似性。

R(qi, d) 表示为: $$ R(q_i,d) = \frac{(k_1+1)·f(q_i,d)}{f(q_i,d)+k_1·(1-b+b·\frac{|d|}{avgdL})} $$ f(qi, d) 表示qi在文档 d 中的词频,|d| 表示文档 d的长度,avgdL是语料库全部文档的平均长度。k1b 为经验参数,一般的k1 ∈ [1.2, 2.0], b = 0.75

假设一共有 n 个文档,按照该公式计算最终一个文档 d 会得到 n 个得分。但是代码中计算的是Score(d, d) ,而且没有求和操作。所以一个文档 d 会由一个vocab_size维度大小的向量表示。按照论文要求,会根据 n 个得分进行降序排列,选 k 个作为文档 d 的近邻文档NN(d) 。复现的代码中则是根据上述向量计算余弦相似度然后选取近邻文档的。

image-20220126205246354

其中term_freq 对应词频f(qi, d)n × vocab_size 大小的矩阵,cosin_similarity(train_docs) 计算文档与文档之间的余弦相似度得分。代码中近邻文档选取了100个

计算idf值代码:

image-20220126210125077

这一处分母应该是(df + 0.5) 。少了一个括号!!!

模型训练测试代码都在一个文件里:nbrreg.py

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
class NbrReg(torch.nn.Module):
def __init__(self, lex_size, bit_size=32, h_size=1000):
super(NbrReg, self).__init__()
self.lnr_h1 = torch.nn.Linear(lex_size, h_size)
self.lnr_h2 = torch.nn.Linear(h_size, h_size)
self.lnr_mu = torch.nn.Linear(h_size, bit_size)
self.lnr_sigma = torch.nn.Linear(h_size, bit_size)
self.lnr_rec_doc = torch.nn.Linear(bit_size, lex_size)
self.lnr_nn_rec_doc = torch.nn.Linear(bit_size, lex_size)

def forward(self, docs):
mu, sigma = self.encode(docs)
# qdist表示语义向量s,服从正态分布 N~(mu,sigma^2)
qdist = tdist.Normal(mu, sigma)
log_prob_words, log_nn_prob_words = self.decode(qdist.rsample())
return qdist, log_prob_words, log_nn_prob_words

# 对应论文中的编码函数
def encode(self, docs):
relu = torch.nn.ReLU()
sigmoid = torch.nn.Sigmoid()
hidden = relu(self.lnr_h2(relu(self.lnr_h1(docs))))
mu = self.lnr_mu(hidden)
# Use sigmoid for positive standard deviation
sigma = sigmoid(self.lnr_sigma(hidden))
return mu, sigma
# 对应论文中解码函数
def decode(self, latent):
log_softmax = torch.nn.LogSoftmax(dim=1)
log_prob_words = log_softmax(self.lnr_rec_doc(latent))
log_nn_prob_words = log_softmax(self.lnr_nn_rec_doc(latent))
return log_prob_words, log_nn_prob_words

模型部分按照论文中的描述,使前馈神经网络就可以实现。值得一提的是 qdist 应该才是文中对应的服从正态分布的语义向量 s。但在生成二进制哈希值时,取的是编码器输出的均值。

训练代码:

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
def train(train_docs, train_cats, train_knn, cv_docs, cv_cats, bitsize=32,
epoch=30, bsize=100, lr=1e-3, latent_size=1000, resume=None,
imp_trial=0):
nsize, lexsize = train_docs.shape
num_iter = int(np.ceil(nsize / bsize))
model = resume if resume else NbrReg(lexsize, bitsize, h_size=latent_size)
model.double()
optim = torch.optim.Adam(model.parameters(), lr=lr)
norm = tdist.Normal(0, 1)
best_prec = 0.0
trial = 0
epoch_range = itertools.count() if imp_trial else epoch
epoch = "INF" if imp_trial else epoch

for e in epoch_range:
model.train()
losses = []
for i in range(num_iter):
print(f"Epoch: {e + 1}/{epoch}, Iteration: {i + 1}/{num_iter}",
end="\r")
batch_i = np.random.choice(nsize, bsize)
np_batch = train_docs[batch_i].todense()
doc_batch = torch.from_numpy(np_batch).double()
knn_batch = train_knn[batch_i]
optim.zero_grad()
qdist, log_prob_words, log_nn_prob_words = model(doc_batch)
doc_rl = doc_rec_loss(log_prob_words, doc_batch)
doc_nn_rl = doc_nn_rec_loss(log_nn_prob_words, knn_batch,train_docs)
kl_loss = tdist.kl_divergence(qdist, norm)
kl_loss = torch.mean(torch.sum(kl_loss, dim=1))
loss = doc_rl + doc_nn_rl + kl_loss
losses.append(loss.item())
loss.backward()
optim.step()
avg_loss = np.mean(losses)
avg_prec = test(train_docs, train_cats, cv_docs, cv_cats, model)
best_prec = max(avg_prec, best_prec)
print(f"Epoch {e + 1}: Avg Loss: {avg_loss}, Avg Prec: {avg_prec}")
if best_prec == avg_prec:
trial = 0
else:
trial += 1
if trial == imp_trial:
print(f"Avg Prec could not be improved for {imp_trial} times, "
"giving up training")
break

return model, best_prec

没有使用GPU!!!kl_loss = tdist.kl_divergence(qdist, norm)​ 计算KL散度。norm 为标准正态分布。

测试代码:

image-20220126214013570

这里 k=100,表示近邻文档取100,这里为test进行二进制哈希映射后,根据汉明距离选取距离最近的k个,然后统计这k个中与test标签相同的数目,相同数目越大表示即准确率越大,模型效果越好。

注意事项

在使用该代码时,需要对数据处理成 20ng-all-stemmed.txt文件里的格式。然后用prepare_data.py 处理生成对应的.mat 文件。将源句子与其复述句标记为相同标签。

  • 固定种子,保证结果可复现。(基本操作)
  • 计算 idf 时,把代码里的小错误纠正了。(分母加了括号)
  • 去掉余弦相似度计算,在已知标签的情况下,近邻文档直接从标签相同的文档中取k个。(bm25已经名存实亡,文档向量用TF-IDF值效果差不多)
  • k值调整,代码中默认100,论文中说为50的时候准确率不在提升,真的是谜之操作。要根据实际情况而定,看每个源句子对应的复述句子的数量,如果k设置过大,则会引入大量噪声。test 函数中的k要与数据处理中的k保持一致,或者小于。(至关重要,不然准确率上不去,而且低到百分之零点几,k=2时,平均准确率有0.43+)
  • 改成了可以使用gpu训练的代码。(至少可以快七倍)
  • 解耦,把训练、测试、模型、数据处理分开。

开始小数据训练,准确率很低。后面就增加数据,准确率依旧那样。开始以为bm25权重计算错误,然后发现代码中 idf 的计算与公式有出入。然后改正了,接着训练,效果还是不好。然后将两份代码对比,发现作者开源的代码里对KL散度值给了一个权重。然后又加权重值,效果还是那样。训练时开始调整knn-size的值,效果好了一点点,但还是很低很低。然后尝试解耦代码,把各个模块代码重新整理,然后发现test 函数里有个参数 k,默认值100,训练一轮后测试模型时,并没有设置该参数,还是默认100。train_knn 的 k 值过大,则会引入噪声,test 中 k 值过大,造成分母过大,准确率很难上去。

论文:GENERALIZATION THROUGH MEMORIZATION: NEAREST NEIGHBOR LANGUAGE MODELS

code:knn-lm

参考链接:香侬读 | 用上文K最近邻特征表示增强语言模型

论文的主要思想是使用传统 knn 算法对预训练神经语言模型进行线性插值扩展。

ps:传统算法在这个深度学习领域的一次融合……印象中,都是使用预训练模型在小数据集上进行微调,这篇论文似乎有点东西。

对于语言模型LM,给定一个上下文序列tokens: ct = (w1, ..., wt − 1) 自回归语言模型通过建模p(wt|ct) 来预测目标词 wt 的概率分布。

kNN-LM可以在没有任何额外的训练情况下,用最邻近检索机制增强预训练语言模型。在模型预训练后,会对训练集的文本集合进行一次前向传播,任何得到 context-target pairs,并将其以键值对形式存储起来(a key-value datastore),以便在推理过程中查找。

image-20211109212234007

具体的:设语言模型为 f(·),可以将一个上文 c 映射为固定长度的向量表示。对于给定的第 i 个训练样本(ci, wi) ∈ D ,定义一个键值对(ki, vi)ki 表示上文ci 的向量表示,vi 表示目标词widatastore (K, V)表示这样一个集合: (K, V) = {(f(ci), wi)|(ci, wi) ∈ D} 在推理阶段,对于给定上文信息 x ,预测 y 概率分布。使用knn算法进行插值,有: $$ p(y|x) = \lambda p_{knn}(y|x) + (1-\lambda)p_{LM}(y|x)\\ p_{knn}(y|x) \propto \sum_{(k_i,v_i)\in N} 1_{y=v_i}exp(-d(k_i,f(x)))\\ $$ λ 表示调谐参数,N表示更具距离得到的k邻近集合。距离计算公式采用欧氏距离(L2范数)。在这里knn只是为了得到集合N。

当然这种使用knn算法的方法不免存在一些算法本身的缺点。一是距离计算公式的选择,二是查询速度,三是k的选择。对于一个预训练语言模型,需要的语料是巨大的,该方法需要将训练集语料的所有键值对保存下来,便于查询。可想而知,从如此巨大的键值对中获取 k 近邻集合N,其查询代价是相当巨大的!!!

正因如此,为了knn-lm更好的work,在实现时,使用了FAISS库来加速查询过程。

一点补充

原本看完论文后,我就知道这个保存的datastore是很大的,但是我没想到这大的如此离谱!!!

readme中提到模型训练使用了8块GPU,而且基于是Fairseq的。脑阔疼,对Fairseq本来就没什么好印象。索性他提供了一个checkpoint,可以跳过模型训练部分了。但看到后面生成datastore时,我。。。

Caution: Running this step requires a large amount of disk space (400GB!). Please read the note about hardware above, before running this!

400GB的磁盘大小!!!!!真的是离了一个大谱!!!!!!

现在想想论文摘要里的那句:

our kNN-LM achieves a new state-of-the-art perplexity of 15.79 – a 2.9 point improvement with no additional training.

这让我不得不怀疑,这sota拼的是磁盘大小啊。真的是有点东西,我一个小作坊,GPU都就是白嫖的,现在整个400G磁盘,我也是活久见。

一个小故事

我本一介凡人,但是一心向往修仙炼丹之术。早闻各路大神每年都会在修仙圣地 ICLR 交流切磋修仙炼丹心得。 一次偶然机会,受高人指点,得到一本秘籍。看完秘籍,豁然开朗,炼丹之路,似乎有了些盼头。

欣喜之余,我也丝毫不敢懈怠。靠着几年的游历经验,白嫖到了一些炼丹器具,也习得一门奇门遁术python,更是窥得仙术tensorflow和pytorch几分奥秘,python大法从入门到入坑,深度学习从入门到放弃,从删库到跑路,我虽自认为资质平庸,在江湖掀不起大风大浪,却也勤勤恳恳苦心修炼,也是到了初识境界。

Github,无数修仙能人术士炫技圣地,在这里果然找到了秘籍之中提到的各种原料以及使用说明书(大神们愿称之为 ‘瑞德密’)。

于是开始每天起早贪黑,备药材,烧丹炉,研究秘籍。按照瑞德密一步一步修炼,但是依旧失败了一次又一次。深感才疏学浅带来的无力,莫不是修为尚浅,无法领略其中奥义。夜不能寐,辗转反侧,我仍百思不得其解。

偶然间,看到到瑞德密后面部分,再次豁然开朗:

欲修此术修此丹药,需备八个丹炉,外部容器非四百G不可。

感觉像是吃了闭门羹,无数人对修仙炼丹之术趋之若鹜,但真正修得正果的,基本是各大财大气粗的门派的人。而对于资质平凡,财力有限的小作坊而言,这条路似乎走的异常艰辛。曾无数次阅读各路大神秘籍,但因为各种苛刻的修炼条件望而却步。

那就告一段落吧。

在平衡内心与周遭的过程中,缝缝补补自己眼中千疮百孔的世界……

很多事都不一样了,在表示同意赞赏nb还行的同时,其实内心也在保持着一些最后的倔强甚至不屑的态度。向往诗和远方的同时,也在吐槽当下糟糕的境况。这是暂时的妥协,而不是最后的结果。

可以预见的是,有一天,我也会被一块大饼圈住,为别人给的蛋糕沾沾自喜,因为天上掉的馅饼开始信奉神明,在推杯换盏中周旋,吃饱了面包然后驻足休息,养老等死,我的墓志铭大概就是我的第一个”hello world”代码。这是我最后的倔强,而不是暂时的妥协。

技术无罪,资本作祟的时代,人人都好像鬼怪,争夺面包,吸食人xie,手捧圣经,说着抱歉,最后还不忘总结,口感似乎差了点……

二十一岁我还在对自己说:管他三七二十一,先做自己想做的事,说自己想说的话,走自己想走的路……

时过境迁,我才意识到,这是原来是叫愤青啊。在深感无力的同时,我也只能长叹一口气(很长很长,用英文就是long long long…)。我还以为我在做自己认为对的事情,我在做自己能做到的事情。

每一次成长,都是和自己谈判的过程,而每次妥协都是在塑造新的自己。over!!!


真的不是在吐槽,有认认真真研读!!!

有关:Integrating Linguistic Knowledge to Sentence Paraphrase Generation 论文解读。

模型框架

典型Transformer-based 结构,编码器和解码器都是多个Multi-head Attention 组成。如图:

image-20211006132242262

大概分为三个部分:Sentence EncoderParaphrase DecoderSynonym Labeling

按照论文里的思路,模型训练包含了一个辅助任务即:Synonym Labeling 。先用encoder部分做辅助任务训练模型,然后整体训练做生成任务。但其实也是可以一起训练的。

下面结合作者开源的代码进行一些分析。

数据处理

  1. 第一步

执行 data_processing.py 脚本,生成一个字典文件 vocab 文件。

1
2
3
4
5
6
7
8
9
10
<pad>
<unk>
<s>
</s>



···
<eos>
<sos>

神奇的是首行的<pad> 并不是脚本添加的,需要自己手动添加,这是运行后面程序发现的。而且多出的<eos>,<sos> 也并没有用到。

  1. 第二步

执行prepro_dict.py 脚本,生成数据集对应的同义词对文件:train_paraphrased_pair.txtdev_paraphrased_pair.txttest_paraphrased_pair.txt

train_paraphrased_pair.txt 为例:

1
2
年景->1 或者->2 年景->4 或->2 抑或->2 要么->2 要->2 抑->2
······

对应train数据集中的第一行是:

1
1929 年 还是 1989 年 ?

具体对应论文中Synonym Pairs Representation 部分:同义词位置对Synonym-position paris

词->pos :其中pos表示sentence 中的位置, 是指同义词,即句子中pos位置上词对应的同义词。年景->1 中位置1处的词为年景 即是的同义词。

  1. 总结

数据处理这一步两个脚本,生成需要的数据文件有:一个词表文件.vocab ,三个同义词位置对文件_paraphrased_pair.txt

整个实验还需要五个数据集文件:train{.src,.tgt},dev{.src,.tgt},test{.src}

最后还想提一下:

tcnp.train.src 第268178行竟然是空行!!!对应同义词对为:<unk>-><unk> !!!

1
2
3
4
5
6
7
8
# tcnp.train.src
268179 阿富汗 肯定 存在 错误 。
268180 在 阿富汗 肯定 有 错误 。
268181 事实上 , 我们 确实 在 阿富汗 犯 了 许多 错误 。
# tcnp.train.tgt
268179 事实上 , 我们 确实 在 阿富汗 犯 了 许多 错误 。
268180 阿富汗 肯定 存在 错误 。
268181 在 阿富汗 肯定 有 错误 。

train 训练

训练部分代码:

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
def train(self, xs, ys, x_paraphrased_dict, synonym_label=None):
# forward
memory, sents1 = self.encode(xs)
_, _, synonym_label_loss = self.labeling(synonym_label, memory)
logits, preds, y, sents2 = self.decode(ys, x_paraphrased_dict, memory)

# train scheme
# generation loss
y_ = label_smoothing(tf.one_hot(y, depth=self.hp.vocab_size))
ce = tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=y_)
nonpadding = tf.to_float(tf.not_equal(y, self.token2idx["<pad>"])) # 0: <pad>
loss = tf.reduce_sum(ce * nonpadding) / (tf.reduce_sum(nonpadding) + 1e-7)
# multi task loss
tloss = self.hp.l_alpha * loss + (1.0-self.hp.l_alpha) * synonym_label_loss

global_step = tf.train.get_or_create_global_step()
lr = noam_scheme(self.hp.lr, global_step, self.hp.warmup_steps)
optimizer = tf.train.AdamOptimizer(lr)
train_op = optimizer.minimize(tloss, global_step=global_step)

tf.summary.scalar('lr', lr)
tf.summary.scalar("loss", loss)
tf.summary.scalar("tloss", tloss)
tf.summary.scalar("global_step", global_step)

summaries = tf.summary.merge_all()

return loss, train_op, global_step, summaries

大概流程就是: encoder -> labeing and decoder

Sentence Encoder

输入:

  • sentence x token [x1,x2,x3,…,xn]

输出:

  • memory: 经过多个Multi-head Attention后的输出

和常规的transformer encoder一样。先是对句子token向量进行embedding,然后添加位置编码positional_encoding

对应代码在:model.py

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
def encode(self, xs, training=True):
with tf.variable_scope("encoder", reuse=tf.AUTO_REUSE):
x, seqlens, sents1 = xs

# embedding
enc = tf.nn.embedding_lookup(self.embeddings, x) # (N, T1, d_model)
enc *= self.hp.d_model**0.5 # scale

enc += positional_encoding(enc, self.hp.maxlen1)
enc = tf.layers.dropout(enc, self.hp.dropout_rate, training=training)

## Blocks
for i in range(self.hp.num_blocks):
with tf.variable_scope("num_blocks_{}".format(i), reuse=tf.AUTO_REUSE):
# self-attention
enc = multihead_attention(queries=enc,
keys=enc,
values=enc,
num_heads=self.hp.num_heads,
dropout_rate=self.hp.dropout_rate,
training=training,
causality=False)
# feed forward
enc = ff(enc, num_units=[self.hp.d_ff, self.hp.d_model])
memory = enc
return memory, sents1

不过有趣的是论文中关于Encoder的部分貌似有些问题:

image-20211006182655905

这是论文中的式子。在transformer中是这样的:Block(Q,K,V) = LNorm(FFN(m)+m)、m=LNorm(MultiAttn(Q,K,V)+Q) 。先add再norm啊!!!

image-20211006183417409

不知道是不是排版错误的原因。主要作者开源的代码里multihead_attention部分还有ffn 部分是先add再norm的。

image-20211006184128806
image-20211006184216337

属实给整蒙了。

Paraphrase Decoder

输入:

  • sentence y token [y1,y2,y3,…,ym] 对应tgt中的句子的token
  • x_paraphrased_dict 引入的外部知识,也就是同义词位置对:synonyms- position pairs
  • memory: 编码器的输出

输出:

  • logits, y_hat: logits是最后一层的输出,y_hat是预测值

这部分有self-attention 、vanilla attention、paraphrased dictionary attention。

self-attention部分和原本的transformer中一样,key=value=query。vanilla attention其实就是key与value相同,query不一样。paraphrased dictionary attention就是引入外部同义词字典知识的部分。这部分主要是计算论文中ct ,按论文中公式来即可。

对应代码在:model.py

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
def decode(self, ys, x_paraphrased_dict, memory, training=True):
with tf.variable_scope("decoder", reuse=tf.AUTO_REUSE):
decoder_inputs, y, seqlens, sents2 = ys
x_paraphrased_dict, paraphrased_lens, paraphrased_sents = x_paraphrased_dict
# embedding
dec = tf.nn.embedding_lookup(self.embeddings, decoder_inputs) # (N, T2, d_model)
dec *= self.hp.d_model ** 0.5 # scale

dec += positional_encoding(dec, self.hp.maxlen2)
dec = tf.layers.dropout(dec, self.hp.dropout_rate, training=training)

batch_size = tf.shape(decoder_inputs)[0] # (N, T2, 2)
seqlens = tf.shape(decoder_inputs)[1] # (N, T2, 2)
paraphrased_lens = tf.shape(x_paraphrased_dict)[1] # (N, T2, 2)

x_paraphrased_o, x_paraphrased_p = x_paraphrased_dict[:,:,0], x_paraphrased_dict[:,:,1]

x_paraphrased_o_embedding = tf.nn.embedding_lookup(self.embeddings, x_paraphrased_o) # N, W2, d_model
if self.hp.paraphrase_type == 0:
x_paraphrased_p_embedding = tf.nn.embedding_lookup(self.embeddings, x_paraphrased_p)
else:
x_paraphrased_p_embedding = paraphrased_positional_encoding(x_paraphrased_p, self.hp.maxlen2, self.hp.d_model)

# Blocks
for i in range(self.hp.num_blocks):
with tf.variable_scope("num_blocks_{}".format(i), reuse=tf.AUTO_REUSE):
# Masked self-attention (Note that causality is True at this time)
dec = multihead_attention(queries=dec,
keys=dec,
values=dec,
num_heads=self.hp.num_heads,
dropout_rate=self.hp.dropout_rate,
training=training,
causality=True,
scope="self_attention")

# Vanilla attention
dec = multihead_attention(queries=dec,
keys=memory,
values=memory,
num_heads=self.hp.num_heads,
dropout_rate=self.hp.dropout_rate,
training=training,
causality=False,
scope="vanilla_attention")
### Feed Forward
dec = ff(dec, num_units=[self.hp.d_ff, self.hp.d_model])

# add paraphrased dictionary attention
h = tf.fill([batch_size, seqlens, paraphrased_lens, self.hp.d_model], 1.0) * tf.expand_dims(dec, axis=2)

o_embeding = tf.fill([batch_size, seqlens, paraphrased_lens, self.hp.d_model], 1.0) * tf.expand_dims(x_paraphrased_o_embedding, axis=1)
W_a_o = tf.get_variable("original_word_parameter_w", [2*self.hp.d_model], initializer=tf.initializers.random_normal(
stddev=0.01, seed=None))
V_a_o = tf.get_variable("original_word_parameter_v", [2*self.hp.d_model], initializer=tf.initializers.random_normal(
stddev=0.01, seed=None))
h_o_concat = tf.concat([h, o_embeding], -1) # N, T2, W2, 2*d_model
score_tem_o = tf.tanh(W_a_o * h_o_concat) # N, T2, W2, 2*d_model
score_o = tf.reduce_sum(V_a_o * score_tem_o, axis=-1) # N, T2, W2
a = tf.nn.softmax(score_o) # N, T2, W2
c_o = tf.matmul(a, x_paraphrased_o_embedding) # (N, T2, W2) * (N, W2, d_model) --> N, T2, d_model

p_embeding = tf.fill([batch_size, seqlens, paraphrased_lens, self.hp.d_model], 1.0) * tf.expand_dims(x_paraphrased_p_embedding, axis=1)
W_a_p = tf.get_variable("paraphrased_word_parameter_w", [2*self.hp.d_model], initializer=tf.initializers.random_normal(
stddev=0.01, seed=None))
V_a_p = tf.get_variable("paraphrased_word_parameter_v", [2*self.hp.d_model], initializer=tf.initializers.random_normal(
stddev=0.01, seed=None))
h_p_concat = tf.concat([h, p_embeding], -1) # N, T2, W2, 2*d_model
score_tem_p = tf.tanh(W_a_p * h_p_concat) # N, T2, W2, 2*d_model
score_p = tf.reduce_sum(V_a_p * score_tem_p, axis=-1) # N, T2, W2
a = tf.nn.softmax(score_p) # N, T2, W2
c_p = tf.matmul(a, x_paraphrased_p_embedding) # (N, T2, W2) * (N, W2, d_model) --> N, T2, d_model

c_t = tf.concat([c_o, c_p], axis=-1) # N, T2, d_model --> N, T2, 2*d_model
out_dec = tf.layers.dense(tf.concat([dec, c_t], axis=-1), self.hp.d_model, activation=tf.tanh, use_bias=False, kernel_initializer=tf.initializers.random_normal(
stddev=0.01, seed=None))

# Final linear projection (embedding weights are shared)
weights = tf.transpose(self.embeddings) # (d_model, vocab_size)
logits = tf.einsum('ntd,dk->ntk', out_dec, weights) # (N, T2, vocab_size)
y_hat = tf.to_int32(tf.argmax(logits, axis=-1))

return logits, y_hat, y, sents2

在最后的输出层部分:论文中的 softmax layer:

yt = softmax(WyConcat[yt*, ct])) 代码中很巧妙的使用 tf.transpose(self.embeddings) 来表示Wy 从而将输出映射到vocab输出。

这里需要注意的是x_paraphrased_dict的表示。论文中叫做synonyms- position pairs,使用P 表示。 P = (si, pi)i = 1M si 表示同义词,pi 表示同义词对应sentence中的位置。训练时,会将si 进行embeddingpi 进行位置编码positional_encoding 。这里的embeddingpositional_encodingencoder部分共享。

Synonym Labeling

输入:

  • synonym_label:同义词标签
  • memory: encoder的输出

输出:

  • logits, y_hat, loss: 一个全连接层的输出、一个预测值、一个损失

synonym_label:[True,False,…],对于给定的一个句子,如果句中词对应位置有同义词这对应label为True,否者为False。这一部分的loss对应论文中的loss2。

这是一个辅助任务,目的是确定给定句子中每个词是否有对应的同义词。有助于更好地定位同义词的位置,结合短语和同义词在原句中的语言关系。可以肯定的是,这是一个二分类任务,并且两个任务共用一个encoder。描述如下:

image-20211006192852063

对应代码:

1
2
3
4
5
6
7
8
9
10
11
12
def labeling(self, x, menmory):
synonym_label, seqlens, sents1 = x
logits = tf.layers.dense(menmory, 2, activation=tf.tanh, use_bias=False,
kernel_initializer=tf.initializers.random_normal(stddev=0.01, seed=None))
y_hat = tf.to_int32(tf.argmax(logits, axis=-1))

# Synonym Labeling loss
y = tf.one_hot(synonym_label, depth=2)
ce = tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits, labels=y)
nonpadding = tf.to_float(tf.not_equal(sents1, self.token2idx["<pad>"])) # 0: <pad>
loss = tf.reduce_sum(ce * nonpadding) / (tf.reduce_sum(nonpadding) + 1e-7)
return logits, y_hat, loss

需要注意的是代码中还有def train_labeling(self, xs, synonym_label=None): 的地方。按照论文中的描述:

image-20211006194032511

这个函数是用来单独做Synonym Labeling 任务的。按论文中的原意,应该是先model.train_labeling(xs, synonym_label) 然后 在进行model.train(xs, ys, x_paraphrased_dict, synonym_label)

但是train.py 代码中并没有这样做。而是直接进行train

有关细节问题

  1. vocab

前面提到过,生成的词表中多出<sos>,<eos> 两个没有用到的词,少了用于填充的<pad>,并且需要自己在首行手动插入<pad> 。猜测可能是课题组的祖传代码。

而且在生成同义词位置对的文件的代码那里,将不在vocab中的同义词统统过滤掉了。

  1. Synonym Pairs Representation
image-20211006200116402

论文里提到如果对应同义词是一个短语,那么就将短语中词嵌入向量求和来表示该短语的向量表示。但是!!!有意思的是,代码中并未体现。而细挖他的数据会发现,给定的数据已是分好词的按空格分隔的。而且是中文数据。中文分好的词,如果是短语,分词后,还是表示一个词。而英文如:abandon同义词give up。give up分词后就是两个单词。也就出现上述情况。

因此,得出结论,作者开源的代码是不完整的。如果换成英文的数据,那么需要考虑的复杂一些了。当然也可以选择把短语同义词过滤掉,那么和中文上处理就是一样的了。

  1. paraphrase_type

paraphrase_type这个是代码中的一个配置参数,默认为1。

parser.add_argument('--paraphrase_type', default=1, type=int)

这是所有参数中为数不多没有help提示信息的参数。并且我相信这也是唯一一个没有help整不明白的参数。释义类型?

data_load.py 中找到了蛛丝马迹:

image-20211006202356732

而这一段代码,也属实有些魔幻。

首先parser.add_argument('--paraphrase_type', default=1, type=int) 这里使用的是1。而取值为1时,对应代码中esle:部分。不用很仔细就可以看出:0时,使用word_set,1时,使用pos_set。我们在观察后面的synonym_label 那一句:

synonym_label = [i in word_set if paraphrase_type else w in word_set for i, w in enumerate(in_words)]

码字到这里,我掐了以下人中,方才缓过神来。接着分析,为0的情况下,那么考虑w in word_set 的真值情况。而for i, w in enumerate(in_words) ,w属于in_words。word_set是什么呢?word_set.add(tem1) 哇哦,是同义词集合诶,in_words是什么?in_words=sent1.split()+['</s>'] 欸,那w难道不是don’t exit in word_set forever?

离谱的是,这里讨论的是paraphrase_type = 1的情况,也就是关word_set屁事的情况。word_set这时都是空的。所以synonym_label 难道不是always be False

Are you kidding me? %$*#@>?*&。。。。

而且x_paraphrase_dic 那里也是有问题的。

x_paraphrase_dict.append([token2idx.get(tem1, token2idx["<unk>"]), token2idx.get(tem2, token2idx["<unk>"])])

这个token2idx.get(tem2, token2idx["<unk>"]) 就很有问题,tem2表示的是pos啊,句子中词的位置,直接给我token2idx 我是无法理解的。

x_paraphrase_dict.append([token2idx.get(tem1, token2idx["<unk>"]), int(tem2 if tem2!="<unk>" else 0)])

int(tem2 if tem2!="<unk>" else 0) 也就是说tem2为<unk> 时,大概就是tem2取0的意思。而tem2出现<unk> 的情况时,tem1也是<unk> 。此时x_paraphrase_dict添加的就是[1, 0] (token2idx[“”] = 1)。举个例子:

1
2
句子x: x1 x2 x3 x4
对应同义词位置对: <unk>-><unk>

这表示这个句子中没有词含有同义词。这个时候x_paraphrase_dict添加[1, 0],就相当于<unk> 为 x1的同义词。这怎么可能?It’s impossible!!! 而且就很不reasonable 。简直离谱!!!离了个大谱。

甚至这段代码的第二个for循环后面的那部分代码逻辑都是有问题的。synonym labeling 任务我认为是有问题的。而将<unk> 与位置0处单词绑定,本身就引入了一些噪声,甚至可能增加<unk> 释义的潜在可能性。至于模型能work,我想,synonym labeling本身作为辅助任务,其loss权值占比为0.1,影响应该是很小的。

这里大胆揣测以下paraphrase_type 意图:

  • paraphrase_type = 0时:x_paraphrase_dict包含句子中的词以及对应同义词,不含位置信息。
  • paraphrase_type = 1 时: x_paraphrase_dict其实包含同义词以及对应位置pos。

无论哪一种情况,其实都是在做一件事:就是将词与其对应的同义词之间进行绑定。第一种更像是比较直接的方式,第二种则略显委婉点。殊途同归!!!

不管是不是这两种情况,其实那块代码都是有问题的。

  1. 有关 paddings

data_load.py 中有段这样的代码,用来获取一个batch size 数据的dataset函数:

image-20211007104255666

其中关于paddings 处的地方自认为还是有些不妥的,对于src、tgt句子进行0填充是正常操作,但是对于x_paraphrased_dict也进行0填充是欠考虑的。

对于x_paraphrased_dict ,0填充,就会出现一部分[[0,0],[0,0],…]的情况 默认将[pad]字符与位置0的词对应了。

总结

这篇论文做的是: Sentence Paraphrase Generation

其中真正核心地方在于 Knowledge-Enhanced ,知识增强。主要就是通过引入外部信息(这里是同义词字典信息)来指导模型生成更加多样性的句子。关于知识增强的方式还是有很多的,这篇论文采用的应该是词表注意力机制,得到外部信息特征表示ct

代码开源了,貌似有没有完全开源!!!

开源的代码基于python3.6 + tensorflow-gpu==1.12。调试起来真的好麻烦。看不惯tf1.x的代码风格,然后用tf2.x复现了下。

  • 对于 paraphrase_type 的两种情况按上述理解做了调整。
  • <unk> 匹配第一个单词的情况进行纠正,将tme2 = <unk> 时(句子中没有词存在同义词的情况),用第一个词与第一个词匹配。即将x1的同义词匹配为x1,这样还是比较妥当的。
  • 过滤了空行,减少不必要的噪声。(空行对应的同义词对为 <unk>-><unk>)
  • synonym_label中使用2进行padding。训练时,是需要对其进行padding的保证输入的数据工整的。比如句子idx会使用0进行填充直到maxlen,而idx=0对应词为 <pad> 。显然,<pad> 和其他词一样,是一个单独的类别了。所以,为了区分,synonym_label的padding_value设置为2。最后做成一个三分类任务。无伤大雅,主要是为了适配句子的填充。
  • 为了适应x_paraphrased_dict 的0填充,对输入句子src的首位置引入一个填充符<pad> 。这一点与第二点先呼应。

不知道效果如何,小作坊,资源有限,训练完要很久很久。敬佩所有敢于开源代码的科研人员,也希望所有开源代码可读性越来越好吧。也希望所有开源代码都能复现结果。至少把种子固定了吧!!!

————————————–——–———-———10月16日更——————-–—-———————————————

一点思考

兜兜转转,模型训练了好几遍,从训练指标来看,loss有下降,acc有升高,但是推理的时候,预测的起始符号<s>后一个词总是结束符</s> 。debug无数遍,优化了一些细节上的小问题,还是出现那样的情况。最后将问题锁定在了padding_masklook_ahead_mask 上,其实最可能猜到就是look_ahead_mask 有问题。此处的mask都是0和1填充的,代码中使用了tf.keras.layers.MultiHeadAttention 接口,对于0、1矩阵的mask,在里面并没有进行mask*-1e9 的掩码操作,这也导致了训练时出现了数据穿越/泄露问题。所以在推理时,输入的起始字符进行预测时不能得到正确结果,至于为什么是结束符,可能是起止符在词表中相邻的缘故。

今天改好后,重新训练,十多个小时过去了,还没训练完(3090,24G显存)。

如今的顶会基本被财大气粗的大公司大实验室的团队承包,小作坊式实验室夹缝求生。顶会期刊也不乏滥竽充数者,实验结果复现难,开源名存实亡……等等一系列骚操作。现有大环境下,一言难尽。

————————————–——–———-———10月17日更——————-–—-———————————————

开完组会,被老师叫停,没有硬件资源,也只好先放弃,把其他事情提上日程。

image-20211006221538257

前言

最近,老师让复现几篇论文中的方法。打开一篇有关cnn 的论文,初略一看,这个模型结构不就是textcnn 吗?!论文中改头换面变成了LS-CNN,着实有些摸不着头脑。那就仔细看看模型说明吧,看看到底有什么神奇之处。

十多分钟后······,大概懂了,LS-CNN = TextCNN(w*stack(A,B)) 。A、B分别表示layer embedding特征、Google word2vec 词向量特征,*表示卷积,stack表示堆叠(两个大小维度相同的矩阵,堆叠后,通道变成2),通过一维卷积操作进行降维(融合两个嵌入特征)。

I know nothing but my ignorance……

2017年谷歌一篇Attention is all you need 在自然语言处理领域炸开了锅。此后transformer 成为了许多人发paper密码 。之后的bert 更是在各大nlp任务上霸榜。各种魔改层出不求。至此,如果不了解transfomer ,不会微调bert 都不好意思说自己是一个 nlper 。不仅如此,隔壁的cv圈都要沾一下光(VIT)。要我说以后投稿就喊一句:哦斯,喊出我的名字吧!transformer. 或者 构筑未来,希望之光,特利迦,transformer type/bert type 。颇有一股新生代奥特曼借力量的趣味(滑稽)。

距离transformer发布已经过去4年,这一波热潮何时褪去,或者下一次革命性的模型什么时候出现,这似乎很难预测。self-attention 的尽头是什么?在这急功近利的时代,各大AI Lab 又有几个愿意沉下心来思考研究呢?毕竟资本家只在乎短期能不能变现。

有意思的是,transformer 又名变形金刚,这也预示这它花里胡哨的各式变形成为可能。

方兴未艾

基于自己有限的认知,随便瞎扯了一下。

回归正题,自然语言处理技术在其他领域的应用正在悄悄进行中,就像开头提到的那个团队所做的工作一样。仔细一想,他们似乎也是在填充这一块空白,为后继者提供一个新的基线,这是有利于领域发展的。这是一个十分优秀的团队,有责任有担当。

而作为新入行者的我或者其他人,应该也是倍感压力的。眼下借助自然语言处理技术发光发热的路子似乎并没有那么简单了。

班门弄斧

所以,在此,不妨大胆预测一下,他们接下来会不会对transformer 那一大家子动手呢,又或者另辟蹊径采用GNN(GCN) 来建模呢?这两种可能性还是很大的。

哈哈哈哈哈哈。在这里挖个坑,献丑提名个 TS-Transformer 来做隐写分析。

采用Transformerencoder 部分提取句子中词与词之间的关系特征和甚至句子的语义特征,然后进行max-poolavg-pool,然后concat 两个pool特征进行融合,在通过最后全连接进行分类。当然对于词嵌入向量也使用两种embedding,即word2veclayer embedding 。基于此实现的TS-Transformer 已经在训练了。事实证明这是可以work的。至于效果,留个悬念,暂不公布,代码暂不开源(就图一乐,/滑稽.jpg)。

【后续补个模型图】

【后续补个实验结果】

似乎使用大规模预训练bert模型来代替word2vec 效果应该更好吧。毕竟word2vec 还是属于浅层特征表示吧。【又挖个坑】

按照这个路子,TS-bert、TS-GNN、TS-GCN...... 都是可能work的。

有空在更

然后……中秋放假了。

哦斯,喊出我的名字吧!TS-Transformer 。构筑未来,希望之光,transformerTS type

【高开低走的特特利迦竟然试图让泽塔串场来拯救低迷的收视率以及低到可怜的评分,笑死】

前言

近日,使用tensorflow的频率比较高,使用过程中也是遇到了一些大大小小的问题。有些着实让人脑瓜子疼。此刻借着模型训练的时间,开始码码字。本来标题想取:

  • 什么?2021了,还有人用Tensorflow?
  • 震惊!代码练习生竟然在用······Tensorflow?
  • 我和tensorflow不共戴天
  • 我想给tensorflow来一大嘴巴子
  • ······

最后,用了这个当我遇到tensorflow2.x时 。无论学习还是生活中,我们都会遇到各种各样的人或事或物。当我们遇到时,会发生什么?我们是会充满期待的。当我遇到···时,我会··· 。这个句式是我喜欢的,大部分人习惯在前半部分大胆设想,后半句夸下豪言壮语。这里取前半句,是因为已经发生了,而省去后半句,恰恰是因为豪言壮语很容易翻车。

这是一篇记录使用tensorflow过程中遇到的一些小而折磨人的问题的博文。但我预言这也将是一篇持久的对tensorflow的血泪吐槽文。

如何看待Keras正式从TensorFlow中分离?

不知道为什么想到了这个知乎话题。六月份的某天,Keras 之父 Francois Chollet宣布将 Keras 的代码从 TensorFlow 代码库中分离出来,移回到了自己的 repo。乍一看,还以为以后tensorflow的keras接口用不了了。但人家只是把keras代码搬回了属于自己的repo。原本的tf.keras 还是能用的。

For you as a user, absolutely nothing changes, now or in the future.

底下全是一片叫好,天下苦tensorflow久已。而我也并不看好这对情侣或者说组合。各自单飞,独自美丽不好吗?keras何必委曲求全做别人的嫁衣。

抛开keras,tensorflow还剩什么?

我想这应该是吐槽后,该冷静思考的问题。而回答这个问题,是需要去阅读官方文档以及实践的。所以,那个句式的后半句也可以是下面的记录。才疏学浅,当厚积薄发。

言归正传,之后遇到的bug都记录在下面部分。

—————————————–———-—-————分割线——————-—————————————————–—

tf.config.run_functions_eagerly(True)

有关Eager Execution 戳这里

然后以下是我粗俗的理解:

这是即时运行和计算图运行相关的概念。即时运行可以让你的程序立马返回结果,计算图运行会先构建计算图(记录你的程序执行行为及顺序),在最后按照构建的图进行计算。

有些晦涩难理解。

模型训练时一般有:

1
2
3
4
@tf.function
def train_step():
with tf.GradientTape() as tape:
···train model code···

这在模型训练过程中是会构建计算图的(具体参考戳这里),构建计算图可以,这时如果在代码中print(x) 一下,就会发现这是没有具体值的,而且没有.numpy() 属性。返回的即计算图中节点的符号句柄 。所以我为什么要在这里print呢?当然是为了调试代码(/滑稽.jpg)。

1
Tensor("x:0", shape=(32, 32), dtype=int32)

官网提到tensorflow2.x是默认开启Eager Execution 的,然而代码中(如上)使用了@tf.function 装饰器,默认以图的方式执行。

1
The code in a Function can be executed both eagerly and as a graph. By default, Function executes its code as a graph.

要关闭默认方式,可以通过设置:tf.config.run_functions_eagerly(True) 来实现。或者干脆不要加这个装饰器。

最后,Eager Execution 增强了开发和调试的交互性,而@tf.function 计算图执行在分布式训练、性能优化和生产部署方面具有优势。简而言之,Eager Execution适合开发过程中调试,@tf.function适合线上部署。

——————-2021.9.17更新———————

自定义

参考->这里

定义模型

抛开keras的sequential, 使用 tensorflow定义模型时,可以有两种继承选择:tf.keras.Modeltf.Module

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TFModel(tf.Module):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.w = tf.Variable(5.0)
self.b = tf.Variable(0.0)

def __call__(self, x):
return self.w * x + self.b
tf_model = TFModel()

class KerasModel(tf.keras.Model):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.w = tf.Variable(5.0)
self.b = tf.Variable(0.0)

def call(self, x, **kwargs):
return self.w * x + self.b
keras_model = KerasModel()

定义训练循环:

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
import tensorflow as tf
import random
import numpy as np

def train_model(x_train,y_train,x_valid,y_valid,model,epochs = 5,batch_size = 64, lr =0.001, print_freq = 10):
loss_object = tf.keras.losses.SparseCategoricalCrossentropy()
optimizer = tf.keras.optimizers.Adam(learning_rate=lr)

train_loss = tf.keras.metrics.Mean(name='train_loss')
train_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='train_accuracy')

test_loss = tf.keras.metrics.Mean(name='test_loss')
test_accuracy = tf.keras.metrics.SparseCategoricalAccuracy(name='test_accuracy')

for epoch in range(epochs):
# 在下一个epoch开始时,重置评估指标
train_loss.reset_states()
train_accuracy.reset_states()
test_loss.reset_states()
test_accuracy.reset_states()
for step in range(int(len(x_train)/batch_size)):
rand_id = np.asarray(random.sample(range(len(x_train)), batch_size))
bs_x_train = x_train[rand_id]
bs_y_train = y_train[rand_id]

# train step
with tf.GradientTape() as tape:
predictions = model(bs_x_train)
loss = loss_object(bs_y_train, predictions)
gradients = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
train_loss(loss)
train_accuracy(bs_y_train, predictions)

# test step
predictions = model(x_valid)
t_loss = loss_object(y_valid, predictions)
test_loss(t_loss)
test_accuracy(y_valid, predictions)

# print info
if step%10==0:
template = 'Epoch {},step {}, Loss: {}, Accuracy: {}, Test Loss: {}, Test Accuracy: {}'
print(template.format(epoch+1,
step,
train_loss.result(),
train_accuracy.result()*100,
test_loss.result(),
test_accuracy.result()*100))

template = 'Epoch {}, Loss: {}, Accuracy: {}, Test Loss: {}, Test Accuracy: {}'
print(template.format(epoch+1,
train_loss.result(),
train_accuracy.result()*100,
test_loss.result(),
test_accuracy.result()*100))

return model

若是继承自tf.keras.Model 则可以使用 model.compile() 去设置参数, 使用model.fit() 进行训练。

1
2
3
4
5
6
7
8
keras_model = KerasModel()
keras_model.compile(
# 默认情况下,fit()调用tf.function()。
# Debug时你可以关闭这一功能,但是现在是打开的。
run_eagerly=False,
optimizer=tf.keras.optimizers.SGD(learning_rate=0.1),
loss=tf.keras.losses.mean_squared_error,
)

——————-2021.9.18更———————

种子

为了确保每次运行结果的稳定,设置固定种子是有必要的。

1
2
3
random.seed(2021)
np.random.seed(2021)
tf.random.set_seed(2021)

——————-2021.9.19更———————

当自定义模型时,继承tf.keras.Model 则需要实现call 方法而不是__call__ 。如果是tf.Module 就实现__call__ 。尽量使用tf.keras.Model ,因为真的很方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class KerasModel(tf.keras.Model):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.w = tf.Variable(5.0)
self.b = tf.Variable(0.0)

def call(self, x, **kwargs):
return self.w * x + self.b
model = KerasModel()
bst_model_path = "./best.model.h5"

early_stopping = tf.keras.callbacks.EarlyStopping(monitor='val_accuracy', patience=5)

model_checkpoint = tf.keras.callbacks.ModelCheckpoint(bst_model_path, save_best_only=True, save_weights_only=True)

model.compile(loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=False),
optimizer=tf.keras.optimizers.Adam(1e-3),
metrics=['accuracy'])
model.fit(x_train, train_y,
batch_size=16,
validation_data=(x_valid,valid_y),
epochs=200,
callbacks=[early_stopping,model_checkpoint]
)

——————-2021.10.4更———————

从python生成器中加载数据

官方文档:consuming_python_generators

code template:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tensorflow as tf

def get_dataset(args, batch_size,shuffle=False):
output_types = (tf.int32, tf.int32, tf.int32, tf.int32)
output_shapes = ((None,),
(None,),
(None,2),
(None,)
)
dataset = tf.data.Dataset.from_generator(generator_fn,
args=args,
output_types= output_types,
output_shapes = output_shapes
)
if shuffle:
dataset = dataset.shuffle(buffer_size = 1000*batch_size)

# dataset = dataset.repeat() 这行代码有毒
dataset = dataset.padded_batch(batch_size=batch_size, padded_shapes=output_shapes, padding_values=(0,0,0,0))
dataset = dataset.prefetch(1)

return dataset

output_types 是必须的,buffer_size 一般取大于等于数据集大小。generator_fn 为生成器函数。如下:

1
2
3
4
5
def count(stop):
i = 0
while i<stop:
yield i
i += 1

——————-2021.10.5更———————

有关call()

子类化tf.keras.Model时,在实现call() 函数需要注意的是接受的参数一般只能是两个:inputstraining

training 一般给用户自定义训练模式提供一定的自由度,training 为布尔类型。当然,training 是非必须的。

1
2
3
4
5
6
7
8
9
class KerasModel(tf.keras.Model):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.w = tf.Variable(5.0)
self.b = tf.Variable(0.0)

def call(self, x):

return self.w * x + self.b

如果模型需要多个输入时:可以通过 inputs = (x1,x2,x3,...)inputs 传入

1
2
3
··省略··
def call(self, inputs):
x1,x2,x3,... = inputs

当然也可以通过**kwargs 将其他数据传入。而不是像这样:

1
2
def call(self, x1,x2,x3,...):
···

类型转换

1
tf.cast(x, dtype=tf.int64)

——————-2021.10.16更———————

tf.keras.layers.MultiHeadAttention

1
2
3
multi = tf.keras.layers.MultiHeadAttention(num_heads=num_heads, key_dim=embed_dim)

out = multi(v,k,q,mask*-1e9)

如果mask是0、1矩阵,记得乘以-1e9 ,否者掩码无效。

TODO