老实说,长久以来我一直抵制使用蓝牙耳机。众所周知,同等价格下蓝牙耳机的音质和有线耳机相比简直是被碾压,让曾经秉承“音质为上”论的我对蓝牙耳机一直嗤之以鼻。而且,这玩意就和充电宝一样,要用的时候永远是没电或者低电量状态。还别提总是需要手动连接设备,以及切换设备非常麻烦(是的,永远别低估了人能有多懒)。我手头上支持蓝牙连接的大法耳机基本都是插线使用,买来试水的小米蓝牙耳机,用了几次就搁置吃灰了。
后来在 B 站看到 UP 主疯狂推荐 Air Pods,自己拿朋友的过来试了下后,觉得,哎呀,真香 ~ 充电盒保证了基本能用一整天,充电速度也快,关键是,打开充电盒盖子就自动连上 iPhone 了,无需任何多余操作,简直懒人福音。虽然音质一般,但是耐不住方便呀。我常对室友讲,Air Pods 是我 2019 年买过的最值得的产品之一了(大概懒是人类文明进步的阶梯吧 2333)。
学生时代我换过了不少手表,从最早的电子表切换到各类石英表,最后又切回 G-Shock 这类复古式电子表,以为找到了真爱。这中间我也尝试过小米手环,然而小米手环给了带来了极差的用户体验,导致我一度对智能手表 / 手环这类产品嗤之以鼻,认为是华而不实的鸡肋。然而,又一个真香定律😂2019 年找暑期实习的时候压力山大,恰好我手机习惯长期保持静音状态,导致错过不少重要电话,头疼不已。Air Pods 的体验告诉我,也许 Apple 的产品就是不一样。买来体验之后觉得,嗯,还不错,电话不会漏接了(正式秋招确实没漏一个电话),充电也没那么烦,watch OS 还不错,还能监督我运动 (虽然没运动几次)。就是成本有点高 23333,整体上感觉中规中矩吧,购买时有点冲动成分在里面。
手头的 15 款 13 寸 mbp 用了 4 年多开始卡顿了,因为保养还不错,计划闲鱼卖掉。考虑到工作之后肯定需要一个在出租屋的日用机,于是计划在毕业季购买一个 mac mini。正好之前万年不更新的 mac mini 在 2018 年底进行了大幅更新,各大网站评测还不错,让我十分心水。在 “24 免息分期等于不要钱” 的理念下,我在苹果官网分期购买了入门款的 mac mini 2018 款 (i5 6 核处理,8G 内存,256G 固态)。对了,分期支付还是用的朋友的信用卡😅。
8G 内存显然是不够用的,然而想要在官网升级到 32G 内存就得多花 4404 大洋,狮子大开口呀!于是我毫不犹豫地去京东购买了两条 16G DDR4 2666Hz 的海盗船内存条回来自己手动扩容了,只需 1100 大洋,相比官方扩容节省不少。这里实名 diss 下米家的 wiha 螺丝刀,手柄太细不好发力,在拧下 mac mini 主板上的两个固定螺丝时费劲九牛二虎之一硬是没拧下来,还险些滑丝,网上一看好多人用米家螺丝刀都翻车了。最后去实验室拿了粗手柄的螺丝刀才顺利卸下这两颗螺丝。
不得不说,mac mini 2018 的体验是相当不错的,毕竟是台式 U,散热空间也比笔记本大。购买前去各论坛调研,很多人不推荐购买 i7 款是因为 i7 降频严重。实际体验时,基本上手上这款 i5 CPU 利用率达到 70% 时,CPU 都超过 90 度了。不是说 mac mini 2018 的散热不好,而是日常使用 CPU 利用率很少超过 70%,真要超过,买 i7 也是降频,还不如省钱买 i5。
用 Geekbench 5 跑分测试 (榜单地址) 发现性能大幅超越平均基线,不知道和双通道大内存有没有关系:
虽然 mac mini 温控基本令人满意,我仍然打算从淘宝购入下图这个静音小风扇,据说在炎热夏季散热效果显著。
趁打折以 1699 购入了这个性价比超高的 4K 显示器,色彩还 OK,出厂自带 25 点校色,色彩偏差实测比较小,漏光控制也还不错,支架支持俯仰和左右旋转。整体使用体验很棒,这个价位下应该超值了。
4K 显示器提高生活幸福感显著,因此在预算允许的情况下,我强烈建议购买 4K 显示器。不过不建议购买淘宝上的便携式 4K 显示器 (包括 gobigger 这种),因为色彩控制实在是太差了,或者是品控不过关,而且价格还普遍不便宜。淘宝上有大量类似的便携式产品,我是吃过亏,不是面板电流声大,就是漏光太严重,或者有坏点,最后全部退货了。
风评很好的一个经典鼠标,就是有点贵。基本上随便去个论坛求推荐 mac 鼠标,MX Master 系列一定人气最高。年底我从闲鱼花 280 购买了一个 95 新的 MX Master 2S,几个月体验下来,满分 5 分的话给个 4 分吧,不至于网上说的那么好。这款鼠标最值得肯定的一点是非常跟手,移动非常细腻精确,在各种奇怪的物体表面上都能胜任,这一点确实是在我至今为止用过的无线鼠标中表现最好的,毕竟最高可达 4000 dpi。鼠标功能键也比较多,配置好了,第一次觉得在 macOS 上其实触摸板也不是刚需了。缺点是,整体的体积有点过大,我花了好久才适应,手小的同学或者女生不推荐购买使用。其次是滚轮有点晃动,而且在无极滚动模式下由于滚轮太灵敏容易回滚,阻尼部分还有可调节的空间。最后就是侧面按键手感有点糟糕。
补充:尴尬!大写的尴尬!我的 Master 2S 在中强度使用刚好半年情况下,出现了左键连击的情况,上网搜索才发现原来有如此多网友同样翻车,甚至我在朋友圈吐槽时,身边刚好也有位朋友一样翻车!看来购买需慎重。最后我怎么解决的呢,作为在沙航磨炼过的 boy, 当然是掏出自己的电烙铁自己修了~
前面提到,原计划二手卖掉手上的 15 款 mbp,结果中间某天 mbp 从桌子边缘滑落至地面,磕缺了一个角,过了两天电池又不争气地鼓包了,在闲鱼估价,这成色已经卖不了几个钱了,算了,那就干脆留着吧。下图可见,电池鼓包已经相当严重了。。。
现在让我花 1500 大洋去 Apple Store 换电池是不可能的了,花费四五百去线下淘宝店换又担心奸商给我垃圾电芯,不如自己动手,丰衣足食。京东搜了下,自营的 macbook 电池就绿巨能和品恒两个品牌,于是花费 340 购买了绿巨能这款电池。
流程参考 iFixit 的指引 ,整体还算顺利,排线拆卸都好说,就是电池粘在金属壳体上要取下来稍微有点费劲。最后更换完的效果如下图:
刚换完电池,使用时可能会出现悬崖式掉电,或者系统提示电池需要修理等现象,于是大量评论斥责垃圾产品,说实话,这些差评让我在购买前比较迟疑。其实,这是因为新电池需要手动校准,并不是产品质量原因。我的做法是,电池放电到 10% 以下后插电充满,如此重复 3 次后,基本恢复正常。新电池还是比较给力的,我的 15 款 mbp 电池续航又恢复到 10 小时以上了,满血复活。系统信息中查询电池容量,超过 mbp 出厂预设 6559 mAh:
海康威视今年在民用固态硬盘领域动作不小,出了像 c2000pro 几个很有诚意的产品。趁打折特价,我以 699 元的价格购入了一块 1TB 容量的海康威视 e200p SATA 固态硬盘,主要用作移动存储设备。不选 M.2 接口是因为发热太大了,不适合长期插在主机上。这款 e200p 硬盘 1TB 版上的 SLC 缓存足够大,TLC 颗粒本身读写速度也很给力,基本上稳定在读 550 MB/s 写 450 MB/s 的水平。作为一款带掉电保护的入门企业级固态,699 的价格确实很香。缺点是,出厂固件比较糟糕,长时间通电会有掉盘现象。解决方案是加入官方 QQ 群,从群文件下载固件手动更新。。。这海康的售后有点奇葩呀,第一次听说加 QQ 群的,固件更新也不放在官网上,很迷😂好在新固件解决了掉盘问题。海康应该感谢我们这些吃螃蟹的。。。
我也不知道怎么称呼这个东西(链接),见下图:
既是充电器,又是充电宝,优先给连接的设备充电,有点中学时代楼道应急灯的感觉。挺不错的,再也不会出现,出门要带充电宝结果充电宝没电的尴尬处境了。五星推荐。
虽然我本人非常讨厌老罗,但是一码归一码,锤科的很多产品还是很不错的,比如这款地平线 8 号行李箱。颜值非常高,滚轮非常润滑同时非常结实,基本是我用过的所有拉杆箱中滚轮质量最好的了,没有之一。整体质量非常过关,坐个正常体重的成人溜两下没有问题。一年下来坐火车、高铁、飞机,没有刻意保护,箱体表面基本没有什么可见划痕。已经推荐给好几个朋友了,用过都说好~
19 年在百度实习的时候看到很多员工桌面上都有这个工作台,全部来自一家淘宝店(链接)。
凳子上坐久了屁股疼,可以把电脑端到工作台上进行站立办公,可手动升降高度,非常方便。回学校后我也买了一个,130 的价格在同类产品中属于最便宜的之一了,用料还算比较实在。
19 年非常火的一双国产跑鞋,匹克态极,被网友称为国产之光。主要是采用了匹克自主研发的黑科技中底材料,好像就叫态极吧,这材料据说“走的时候软,跑的时候弹”,穿在脚上缓慢行走有“踩屎”的快感。鞋盒里面甚至还提供了这种材料的样品供把玩,体验后,觉得确实是挺好玩的一种非牛顿流体,软起来捏橡皮泥,硬起来可以当乒乓球打。
我和室友一起购买了匹克态极和天猫的联名款。怎么说呢,鞋底确实挺软挺舒服的,有“踩屎”的感觉,但是把控不到位,导致给脚掌的支撑性不足,容易崴脚。整体做工也不算出色,外观比较丑😂行吧,就算支持国货了。
过去一年还租了两台 VPS,一台小众的美国 VPS 历经风雨从未被墙,一台从网友手上低价收的一台香港 VPS,保证了 24 / 7 科学上网的可用性。另外还购买或续费了很多正版软件产品或服务,如 macOS 上的 transmit, dash, bartender, 1password, papers 等。音乐平台上,抛弃了 Apple Music 和网易云,投入了 QQ 音乐的怀抱。
]]>其实,只要手头有一台 VPS,可以很方便地搭建一个私有网盘,满足自己的各种需求。GitHub 上有很多成熟的开源网盘方案,如 NextCloud 等,这里不介绍它们的安装使用,因为它们配置相对复杂 (小白不友好),系统资源消耗大 (vps 小鸡不友好)。这里推荐两个比较轻量的解决方案:File Browser 和 Updog,基本上都是一行命令搞定。
File Browser 是一个 GO 语言编写的 WEB 文件管理器,提供了文件上传、下载、删除、预览、重命名、分享等诸多功能,同时支持多用户管理,功能非常齐全。它的界面如下图:
File Browser 的使用非常简单,从 Github release 界面下载对应 linux 发行版安装包解压,其实就是一个名为 filebrowser 的可执行文件,无需任何依赖,大小仅 30 M+,非常轻量。
部署仅需一条命令行:
filebrowser -a 0.0.0.0 -p 8007 -r /root -d /root/filebrowser.db
这样就指定 /root
目录为网盘根目录,在 8007
端口下启动了一个私有网盘。-d
命令指定了存放用户信息的数据库的目录,第一次运行会自动生成。访问 http://your_vps_ip:8007 就可以登入刚刚搭建好的网盘,默认用户名和密码均为 admin
,登入后在设置里面可以更改。
用 nohup 或者 screen 等可以让 File Browser 后台运行,不过为了更方便地使用,我建议使用 systemd 新建一个开机启动的守护进程服务,后台常驻 (因为确实好用):
在 etc/systemd/system
下创建 fb.service
文件,内容填写:
[Unit]
Description=filebrowser service
After=network.target
[Service]
Type=simple
Restart=always
ExecStart=path_to_filebrowser -a 0.0.0.0 -p 8007 -r /root -d /root/filebrowser.db
[Install]
WantedBy=multi-user.target
注意上面的 file_to_filebrowser
填写 File Browser 的绝对路径。
执行以下两条指令使服务生效并启动:
sudo systemctl enable fb.service
sudo systemctl start fb.service
前面我提到 filebrowser 非常轻量,在我的 VPS 上,用 systemctl 查看,该服务仅仅占用 18 M 内存而已:
文件分享
我非常喜欢 File Browser 的文件分享功能,它可以指定分享外链在一段时间后失效:
直链下载
前面文件分享获取链接后,复制该链接在浏览器新页面打开,在文件名上右键选择复制链接地址
即可得到直链地址。
命令行上传下载
在 GitHub 的 issues 里面,作者的多次回复表明他似乎并不希望用户使用命令行进行文件上传和下载 (这点我实在想不通),然而作者依然透露了 api 接口,这样,我们可以通过简单的 http post 上传文件。然而我发现作者提供的 api 在版本 2.0 后就发生了变动不再有效,通过简单的分析,我获取了新版的 api 的正确使用方式。我手头上两台 VPS 之前安装的 File Browser 版本为 2.0.15 和 2.0.16,以下方法上传文件均是成功的:
利用 curl 命令通过 post 上传文件:
首先我们要获取用于认证的 token:
# 利用 post 命令,传入正确的用户名和密码,获取认证 token,并存入变量 TOKEN
TOKEN=$(curl -X POST --data '{"username":"your_username", "password":"your_password"}' http://your_vps_ip:your_port/api/login)
假定 File Browser 指定的文件根目录下有个文件夹 test_dir,要把本机当前目录下的文件 test.png 上传到远端 test_dir 下:
# 这里加 -o 重定向到 /dev/null 是为了显示上传进度,可省略
curl -X POST \
--header "X-Auth: $TOKEN" \
-F "file=@test.png" \
-o /dev/null \
http://your_vps_ip:your_port/api/resources/test_dir/test.png
效果如下图:
默认设定是如果文件已存在,则不进行覆盖。可以添加参数 override=true
覆盖旧文件:
curl -X POST \
--header "X-Auth: $TOKEN" \
-F "file=@test.png" \
-o /dev/null \
http://your_vps_ip:your_port/api/resources/test_dir/test.png?override=true
同样,下载也可以类似进行,不过我觉得还是通过分享链接获取直链下载比较方便。
Updog 是我昨天发现的一个极简 HTTP server, 官方的介绍是用用于替代 Python 内置的 SimpleHTTPServer,初步使用后觉得还不错,比 SimpleHTTPServer 多了文件上传和文件预览的功能,同时 UI 上也更好看一些。
显示效果:
最上方显示 Updog 的 logo, 以及启动服务器的根目录路径,下面是文件列表。单击文件名开始下载,单击每一项右侧的 View in browser
可以进行简单的文件预览。
安装方法极其简单:
pip3 install updog
部署同样只需要一条指令:
updog -d /your_directory -p 1234 --password 123
这样,就在指定的目录下启动了一个简单的 http server, 端口是 1234, 密码是 123 (用户名留空,浏览器登入时不填写)。省略 --password
则不设置密码。设置开机后以守护进程自启动参考 File Browser 的设定,只需修改 ExecStart
命令。
命令行下载
Updog 没有像 File Browser 那样的文件分享功能,不过在需要分享的文件的目录下临时启动一个 Updog 服务也能达到类似的效果。
命令行下载比较简单,在需要的文件上右键拷贝链接,使用 wget 进行 http 认证下载即可:
# --continue 开启断点续传,防止网络不稳定下载中断
# 假定下载根目录下的 test 文件
wget --continue \
--http-user="" \
--http-password="your_password" \
http://your_vps_ip:your_port/test
命令行上传
对 Updog 的源码进行简单的分析发现,我们同样可以用 curl 命令实现用户认证和文件上传。
以上面显示效果图为例,假定我们要把当前本地目录下的 test.py 文件上传到远端 tools 目录下,curl 上传的命令是:
# 注意这里的 path 是绝对路径
# 这里加 -o 重定向到 /dev/null 是为了显示上传进度,可省略
curl -X POST \
-F file=@test.py \
-F path=/home/scotfree/tools \
-o /dev/null \
-u :your_password http://your_vps_ip:your_port/upload
题外话:现如今,GFW 技术越来越强,不搞点高级的过墙方法还真容易被干扰。很多新兴协议新增了探测防御的功能,遇到主动审查直接跳转至本机的网盘服务即可,那么本文提到的 File Browser 和 Updog 就是很好的私有网盘实现方案。这样同一个域名,既可以用于过墙,又可以用于私有网盘,使用方便,安全性相对更好。事实上,在这套方案下,我的多台 vps 挺过了多次敏感时期,从未被墙稳如老狗,哈哈。
]]>我们知道,Docker 在 Linux 上利用了 Linux 原生支持的容器方式实现资源和环境的隔离,直接利用宿主内核,性能接近原生。然而,在 macOS 上却仍然需要虚拟化的技术。早期的 Docker 干脆直接在开源的 VirtualBox 中构建虚拟机,性能低下。后期的 Docker 基于轻量化的虚拟化框架 HyperKit 开发,该框架又是 macOS 10.10 后 Apple 官方发布的 Hypervisor.framework 二次开发,据说性能得到很大提升。
作为商业化虚拟机的佼佼者的 Parallels Desktop,提出了自己的 Parallels Hypervisor。因此,Docker 和 Parallels Desktop 在虚拟化技术上谁更胜一筹呢?或者说,开源和商业化闭源的虚拟化技术谁更强?通过一番测试,我的结论是 Parallels Desktop 完全吊打 Docker 。
估计大部分的用户和我一样,运行 linux 虚拟机不需要图形界面,仅仅需要一个 ssh 通过终端连接即可。Parallels 官方的这份指引介绍了如何打开 Parallels Desktop 的 headless mode。基本按照下图设置就行:
官方的 headless mode 就是让虚拟机以后台进程的形式运行,同时不显示 GUI 界面。注意这里并不是终止了 GUI 界面相关进程,而仅仅是不显示而已,就好比一个电脑主机没有插上显示器。以 Ubuntu 18.04 为例,打开 headless mode 通过 ssh 后登入虚拟机后,我们通过系统活动监视器会发现虚拟机内存占用比较高,约 1G 左右。而通过 htop 命令显示虚拟机内的内存占用如下图:
可以发现,与图形界面相关的 gnome-desktop 套件占用了大量内存。由于图形界面的存在,负责虚拟机与 macOS 宿主通信的 prl_disp_service
也会相应占用更多内存,而且 gnome-desktop 套件经常自动触发后台更新与维护服务导致 prl_disp_service
占用大量 CPU (这就是为什么 Linux 虚拟机经常在用户没有任何操作的时候 CPU 保持高占用,风扇狂转)。
如何关闭掉图形界面呢?我们知道,不同的 linux 发行版采用不同的桌面环境 (Destop Environment, DE), 依次去手动指定关闭比较繁琐。况且,ubuntu 17 之后采用的 gnome-desktop 桌面环境包含了大量特定组件,以往通过 DM (Display Manager) 去关闭 DE 的方式仍然会残留大量 gnome 相关的进程,浪费内存。
早期 Linux 系统的 init (PID=1 的 root 进程) 的实现 SysV init 中提出了 run-level 的概念,run-level = 2, 3, 4 代表多用户非图形化界面,run-level = 5 代表多用户图形化界面。近年来,主流 Linux 发行版的 init 基本替换为了更先进 systemd。systemd 向下对 SysV 兼容,run-level = 2, 3, 4 对应于 multi-user.target, run-level = 5 对应于 graphical.target。systemd 的最大特点是面向目标 (target), 我们定义一个要启动的目标并注明它的依赖条件,systemd 负责满足所有依赖条件并执行目标。通过查看 graphical.target 的依赖链,我们发现,系统要启动图形界面,首先启动 multi-user.target 多用户非图形化界面,然后启动 display-manager.service,再由 display-manager.service 的依赖链依次往下触发大量相关服务。因此,我们只需要在系统启动时,只启动到 multi-user.target 即可,这样的话可尽最大可能避免启动不需要的 GUI 相关服务。
操作方法:终端运行
sudo systemctl set-default multi-user.target // 默认是 graphical.target
# 然后重启
sudo reboot
此时再 ssh 登入虚拟机会发现内存占用降低到 100M 出头,同时 prl_disp_service
因为没有了图形界面内存占用也会降低到可以忽略不计。下图是 ubuntu 虚拟机内部的内存占用和 macOS 系统显示的虚拟机内存占用 (我进一步移除了占内存大概 10M 的 snapd 服务进程):
可以看到,关闭图形界面相关的进程后,Parallels Desktop 中的 ubuntu 虚拟机内存占用得到极大改善,100M 出头的内存占用几乎可以忽略不计,甚至比随便一个 mac 原生应用还轻量。更牛逼的是,前面提到,Parallels Desktop 支持让虚拟机以后台进程的形式运行,也就是这时我们可以完全退出 Parallels Desktop 主程序而保持虚拟机本地活跃,依然可以 ssh 登入虚拟机,太强了!
下面,我将做些简单的测试,从各个维度对 Parallels Desktop 和 Docker 展开对比。
硬件平台: mac mini late 2018 i-8500B 32G ram
虚拟机平台: Parallels Desktop 15.1.2 (47123), Docker 2.2.0.3(42716)
虚拟对象: Ubuntu 18.04.1
为保证公平对比,虚拟平台的设置均为 CPU 2 核,内存 1G,虚拟内存 2G:
(1) 启动速度对比
Parallels Desktop: 启动主程序,大概不到 3s。Ubuntu 冷启动大概 14s, 从休眠态启动大概 1s (秒启动)。
Docker: docker 主进程启动大概 12s,Ubuntu 镜像启动速度忽略不计。
结论: 在真实使用中,Parallels Desktop 上用户肯定会采用从休眠态恢复虚拟机,因此这里 Parallels Desktop 胜出,而且体验好太多。
(2) 内存占用
Parallels Desktop: 主程序内存占用 100M 左右 (主进程可退出),虚拟机内存占用 100M 左右(见第一小节图片),负责 macOS 与虚拟机通信的后台守护进程合计占用 30M 左右 (如下图):
Docker: 仅仅启动主进程,不启动任何容器,docker 依赖的 hyperkit 内存占用居然超过 1.3 G (见下图)!ubuntu 容器内存占用 250M+。
结论:Parallels Desktop 完胜,不解释。Docker 主程序打开什么也不干就常驻 1G + 内存,体验实在是太差了。
(3) 磁盘 IO:
利用 dd 命令测试 1G 文件下的磁盘写入速度:
dd bs=1M count=1K if=/dev/zero of=test oflag=dsync && rm test
由于 macOS 上的 dd 命令 (BSD 系) 和 ubuntu 不一致,因此利用 homebrew 安装 GNU 版本的 dd: brew install coreutils
,保持和 ubuntu 的一致性。GNU 的 dd 命令路径在: /usr/local/opt/coreutils/libexec/gnubin/dd
。
分别执行三次读写速度测试命令,对比原生 macOS, Parallels Desktop,Docker 三者如下:
可以发现, Parallels Desktop 下虚拟机内的磁盘写入速度非常接近原生 macOS, 而 Docker 就惨不忍睹了。经常在网上看到无数人吐槽 Docker 磁盘 I/O 缩水太严重,今日小测果然如此。
结论:Parallels Desktop 完胜,不解释。
(4) CPU 性能:
对 1G 文件进行 md5 运算,作为粗略性能比较:
dd if=/dev/zero bs=1M count=1K | md5sum
速度对比如下:
可以看到,Parallels Desktop 的 CPU 利用效率也是比 Docker 高出不少。另外,比较神奇的是,原生 macOS 用 md5sum 测试的性能居然不如 Parallels Desktop,是因为 macOS 做了特殊的设定还是什么原因我表示不清楚,毕竟 macOS 基于 BSD,有很多隐藏差异可能我并不知情。
结论:Parallels Desktop 完胜,不解释。
总结: macOS 上使用 Linux 环境,建议采用 Parallels Desktop 虚拟机而不是 Docker, 前者资源占用更少,性能更高。可以看出 Parallels Desktop 作为 macOS 上虚拟化技术的一哥,确实做的出色。这也难怪有用户在论坛 提问 Apple Hypervisor 和 Parallels Hypervisor 谁更强时,客服霸气回复 Parallels Hypervisor is the best one.
另外,本文没有使用 VMware 虚拟机是因为在本人的日常体验中,VMware 相比 Parallels Desktop 有着肉眼可见的差距。
2020.2.27 更新: Docker on Parallels
经评论区有位朋友提醒,可以将 Docker 建立在 Parallels 上使用,我就去调研了一番。
在 Docker 版本 1.12 之前,要想在 macOS 上运行 docker,就必须依赖 Docker Machine 这个工具。Docker Machine 允许我们将构建与管理容器的 Docker Engine 建立在虚拟机上,这样我们就可以让 Docker 在多个平台上运行。版本 1.12 前的 Docker 使用 Docker Machine 构建与 VirtualBox 虚拟机之上,因此性能造人诟病。后来的版本基于 Apple 提出的 Hypervisor.framework 开发,被称为“原生” Docker 应用。
因此,依赖 Docker Machine,我们可以直接在 Parallels Desktop 上运行 Docker。从 Parallels Desktop 官网 上看到,Docker Machine 并未完全支持 Parallels Desktop,想要尝鲜可以去 docker-machine-parallels 页面安装使用。
安装 docker-machine-parallels 比较简单:
brew install docker-machine-parallels
整个 docker-machine-parallels 工具才 12 MB。
终端运行:
docker-machine create --driver=parallels prl-dev
就可以构建一个基于 parallels hypervisor 的名为 prl-dev 的 Docker 客户端。从执行过程我们可以看到,中途下载了大概 60M 左右的 boot2docker,相关文件均在 /Users/$USER/.docker
目录下。
打开 parallels desktop 软件,我们可以看到多了一个 docker 虚拟机:
点击开关按钮启动此虚拟机 (等价于命令行执行 docker-machine start prl-dev
)。
终端执行 docker-machine env prl-dev
,输出一串自定义环境变量命令,复制这些命令终端输入,我们就可以愉快地运行 docker-machine-parallels 了。此时终端执行 docker
调用的将会指向我们构建的 prl-dev
了。
我们再来进行相关简易性能测试:
(1) 启动时间:启动 prl-dev
在我的 mac mini 大概耗时 17s 左右。
(2) 内存占用:prl-dev
本质上是一个虚拟机,在不运行任何容器的情况下,prl-dev
进程使用内存大概 80M 左右,确实比 Docker Mac 版好太多。
启动一个 ubuntu:18.04 容器,由于官方镜像组件比较少,容器的内存占用约 60M。
(3) 磁盘 IO:
利用 dd 命令测试 1G 文件下的磁盘写入速度,结果如下图:
可以看到此时磁盘 IO 性能较 Docker Mac 版有了显著提高,基本非常逼近前面的 Parallels Ubuntu 虚拟机了。由于差别比较小,为了防止是硬盘的正常 IO 波动,我通过多次测试,发现确实 prl-dev
比 Parallels Ubuntu 虚拟机在磁盘 IO 的写入性能上稳定低 100MB/s左右。
(4) CPU 性能:
对 1G 文件进行 md5 运算的结果如下:
同样是较 “原生” 的 Docker Mac 有明显提升,但仍略低于 Parallels Ubuntu 虚拟机。
至此,我们得出结论,Parallels Hypervisor 的技术确实不错,基于它构建 Docker 比官方的原生 app 有明显性能提升,且更省内存资源。另外,磁盘空间占用也更省了 (~440M vs 2G)。
]]>今天无意中在网上看到了纽约州立大学石溪分校计算机实验室的 LaTeX Guidelines,才发现原来可以使用标致串(Identification String)的小技巧来解决这一痛点,即把 Booktitle 后的字段统一替换为自定义的 Identification String 来提高效率,避免重复操作。举个例子,经过这么一番操作后,ref.bib 文件中的引用条目会变成这样:
@inproceedings{Buch:2017SST,
author = {Buch, Shyamal and Escorcia, Victor and Shen, Chuanqi and Ghanem, Bernard and Niebles, Juan Carlos},
title = {SST: Single-Stream Temporal Action Proposals},
booktitle = CVPR,
year = {2017},
pages = {6373--6382},
publisher = {IEEE}
}
这里的 booktitle 后的 CVPR 就是一个 Identification String,注意没有大括号包围。它实际上是一个占位符,或者说变量,需要额外定义。因此,我们可以新建一个新的 bib 文件(不妨命名为strings.bib),里面是各种 Identification String 的定义:
@string{CVPR = "Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition"}
这样在 tex 文件末尾导入 bib 引用的时候,多导入一个 strings.bib 即可:
\bibliography{strings,ref}
这样,针对不同的期刊/会议的引用格式要求,我们每次只需要修改 strings.bib 文件即可,效率一下得到极大提高。
当然,这样还有一个好处,那就是在使用文献管理软件时,文章来源一律写成期刊/会议简写即可。比如我使用的 Papers 文献管理软件,对于 Conference 那一栏直接写成对应会议的简写就好:
这样日常的查阅,筛选,管理也会更轻松。
]]>吐槽部分:这一部分学完后感触很深,tf 真的很难,它已经从一个 Python 库上升到了一个十分复杂的编程语言的高度,让人抓狂。大部分 API 文档是根据代码注释生成的,往往晦涩难懂,由于时间和精力原因去深入庞杂的 C++ 底层几乎不可能。由此带来的最大困扰是,you seldom know what is happening under the hood。比如有多少人知道 tf.image 模块的 resize 的插值方法中,align corners 和主流图像库处理的方式都不一样?在学习这部分的过程中,我翻阅了大量博客,Github Issue,Stack Overflow 回答,多次对着源码逐行分析并进行代码测试,最终总结成个人笔记可谓是“满纸荒唐言,一把辛酸泪”。不过,也正是学习这部分的钻研过程中,感慨 tf 真的是为了实际应用做了充分的考虑,正如 caffe 作者贾扬清在知乎说道: "TF的确难,但是它给你提供了真正可以产品化的可能性。"
先把这部分我个人觉得要注意的一些点(或者说是大坑)列举一下,方便日后查阅:
推荐阅读: CS230 课件 “An overview of tf.data” 部分列举的所有链接。
TFRecord 文件的数据是通过 tf.train.Example 这个 protobuf 的格式存储的。其定义在 tensorflow/core/example/example.proto 和 tensorflow/core/example/feature.proto :
message Example {
// type: Features, name: features
Features features = 1;
};
message Features {
// Map from feature name to feature.
// Features is a key-value store, where each key (string)
// maps to a Feature message
map<string, Feature> feature = 1;
};
// Containers for non-sequential data.
message Feature {
// Each feature can be exactly one kind.
oneof kind {
BytesList bytes_list = 1;
FloatList float_list = 2;
Int64List int64_list = 3;
}
};
// Containers to hold repeated fundamental values.
message BytesList {
repeated bytes value = 1;
}
message FloatList {
repeated float value = 1 [packed = true];
}
message Int64List {
repeated int64 value = 1 [packed = true];
}
tf.train.Example 中包含名为 features 的 Features 的信息,每个 Features 中包含一个从 feature 名称到 feature 属性 (Feature) 的字典映射(key-value对),其中每个 Feature 的可以从 BytesList (字符串),FloatList (浮点实数列表), Int64List (整数列表)中取。
一个 tf.train.Example 的例子:
features {
feature {
key: "age"
value { float_list {
value: 29.0
}}
}
feature {
key: "movie"
value { bytes_list {
value: "The Shawshank Redemption"
value: "Fight Club"
}}
}
feature {
key: "movie_ratings"
value { float_list {
value: 9.0
value: 9.7
}}
}
}
样例程序:
import tensorflow as tf
import os
import cv2
# 输出的 TFRecord 文件带路径的名称
output = './output.tfrecords'
# 创建一个 writer 来写入 TFRecord 文件
writer = tf.python_io.TFRecordWriter(output)
# 辅助函数,生成整数和字符串型的 tf.train.Feature。
def _int64_feature(value):
return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))
def _bytes_feature(value):
return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
# 读取图片,并保存到 TFRecord 文件中
img_dir = './img'
imgs = os.listdir(img_dir)
imgs.sort()
for index, img in enumerate(imgs):
img_path = os.path.join(img_dir, img)
img_data = cv2.imread(img_path)
resized_img = cv2.resize(img_data, (128, 128, interpolation=cv2.INTER_AREA))
# 这里必须把 ndarry 转换成字符串形式的原始二进制数据流
img_raw = resized_img.tostring()
# 生成 Example Protobuf 文件
example = tf.train.Example(features=tf.train.Features(feature={
'shape_': _int64_feature(resized_img.shape[0]),
'label_': _int64_feature(index),
'img_raw': _bytes_feature(img_raw)
}))
# 将序列化后的example 写入 TFRecord 文件
writer.write(example.SerializeToString())
writer.close()
import tensorflow as tf
# 注意默认 shuffle = True
# 返回一个队列 Queue 对象
filename_queue = tf.train.string_input_producer(['./output.tfrecords'], shuffle=False)
# 创建一个 reader 读取 TFRecord 文件中的样例
reader = tf.TFRecordReader()
# 一次读取一个样例。也可以使用 read_up_to 函数一次读取多个样例
# Returns the next record (key, value) pair produced by a reader.
_, serialized_example = reader.read(filename_queue)
# 解析单个 features 文件; 解析多个用 parse_example 函数
features = tf.parse_single_example(
serialized_example,
features={
'shape_': tf.FixedLenFeature([], tf.int64),
'label_': tf.FixedLenFeature([], tf.int64),
'img_raw': tf.FixedLenFeature([], tf.string)
})
# 以上 tf.FixedLenFeature 方法解析的结果为一个 tensor。另一种方法是 tf.VarLenFeature, 解析的
# 结果是 SparseTensor
# 将字符串 tensor 解析成数据 tensor,注意此时为一维数据,需要 reshape
image = tf.decode_raw(features['img_raw'], tf.uint8)
image_shape = tf.stack([shape_, shape_, 3]) # 这一行不能少
image = tf.reshape(image, image_shape)
# 默认是 int64 的 tensor, 转成 int32
shape = tf.cast(features['shape_'], tf.int32)
label = tf.cast(features['label_'], tf.int32)
sess = tf.Session()
# 多线程部分参见第3部分
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
for i in range(4):
print sess.run([image, shape, label])
读取示例:
tf.InteractiveSession()
image_raw_data = tf.gfile.FastGFile('./imgs/img1.png', 'rb').read()
# 一定要明确指定3通道,默认自动识别好像有bug,不生效
img_data = tf.image.decode_png(image_raw_data, channels=3)
# (732, 808, 3) uint8
# 格式: [H, W, C]
print img_data.eval().shape, img_data.eval().dtype
注意:
(1) tf.gfile 模块为 TensorFlow 的文件读写模块,C++ 实现,API 与 Python 自带文件模块高度相近。区别在于,tf.gfile 实现了多种文件系统的读写,比如本地文件,谷歌云存储文件(前缀 gs://),HDFS 文件(前缀 hdfs://) 等等。TensorFlow 中写入载入 checkpoints,TensorBoard 日志文件等都是用 tf.gfile 模块实现。
简言之,tf.gfile 实现了更多文件读写接口,在处理日常本地文件读写时,tf.gfile 并没有明显的速度优势,用 python 自带文件读写模块就可以了。
(2) tf.gfile.GFile, tf.gfile.FastGFile 二者在r1.8源码实现上并无区别,都是没有线程锁的文件 I/O,所以二者等同。
(3) 图像编码时的搭配:
tf.decode_raw()
函数;如上面的例子,可以改成下面的写法:
image = cv2.imread('./imgs/img1.png')
print image.shape # (732, 808, 3)
image_raw_data = tf.decode_raw(image.tobytes(), tf.uint8)
print image_raw_data.eval().shape # (1774368,) // 1774368 = 732 * 808 * 3
注意 tf.decode_raw() 里面第二个形参 out_type 一定要正确,否则输出的数据的维度就不对。
(4) tf.image 中解码图像的 API:
上面的函数 tf.image.decode_png 等返回的 tensor 是有静态 shape的,而下面的 tf.image.decode_image 由于使用了 tf.cond 判断图片类型,因此返回的 tensor 没有静态 shape,造成它解码的图片无法与tf.image.resize_images() 一起使用;
现在 tf.image.decode_png,tf.image.decode_jpeg 已经能读取所有图片文件类型,和非动态 gif 文件了;
务必明确指定 tf.image.decode_png() 等函数中的 channels,想要 RGB 图务必指定为 3,默认的0在 1.8 版本并未生效。
--> 总结: 使用 tf.image.decode_png() 函数,不要使用 tf.image.decode_images() 函数,注意 channels 参数手动指定。返回的数据类型为 tf.uint8。
(5) 图像的存储 API:
encoded_image = tf.image.encode_png(img_data)
with tf.gfile.FastGFile('test.png', 'wb') as f:
f.write(encoded_image.eval())
函数第一个参数为图片数据,shape: [height, width, channels],数据类型必须为 uint8。同时, 两个函数中提供了图片品质压缩参数,encode_png 为 compression,0-9 之间取值,数值越大压缩越严重;encode_jpeg 为 quality,0-100之间取值,数值越大质量越好。
说明:后续的图像操作,很多只接受浮点图像数据,有些先把图像转成浮点,处理完成后再转为原来的数据类型;如果有多个图像处理操作,来回在 uint8 和 float32 之间的转换会导致精度损失,因此建议在图像处理之前先统一转换成 float32 类型:
img_data = tf.image.convert_image_dtype(img_data, tf.float32)
输入:
images: 4D 的 [N, H, W, C] 或 3D 的 [H, W, C] 数据。因此这个函数支持批处理。
size: [new_height, new_width]
method: 默认双线性插值。可选0-3。0:双线性插值;1:最近邻法;2:双三次插值;3:面积插值
align_corners: 角度是否对齐。`记得务必设置为 True`
说明: tf 中的图片 resize 和 opencv,PIL 等主流库的实现不一样。opencv 等主流库在插值计算时,对齐时是把每个像素看做一个“点区域”,因此用的是中心点对齐,有个 0.5 的偏移计算设置,可参考 CSDN博客;而 tf 在实现的时候,每个像素就是一个点,align_corners 设置为 False 就是上述博客中没加偏移的情况,显然不合理;align_corners 设置为 True 就是对齐四个角顶点,连续插值。个人觉得 tf 的设置 align_corners=True 更加合理。代码的区别可参加图: https://i.loli.net/2018/08/16/5b755dc364464.png。这一部分的相关讨论参见: https://github.com/tensorflow/tensorflow/issues/6720。
若要使用 opencv 的 resize 函数,需使用 tf.py_func 包装起来:
# img_data is a tensor
img = tf.py_func(lambda input: cv2.resize(input, (4, 4)), [img_data], tf.float32, stateful=False)
print img.eval()
输入: image: 4D 的 [N, H, W, C] 或 3D 的 [H, W, C] 数据。因此这个函数支持批处理。
生成一个大小为 [height, width] 的图,图标图像小于原图就裁剪,否则周围填0。注意只是裁剪或填充,没有插值。
输入的 image 为 3D tensor。因此不能批处理。central_fraction为 (0, 1] 之间的浮点数。
做中心裁剪,central_fraction 为长和宽裁剪出的比例。比如 [100, 100] 的原图,central_fraction=0.5,那么输出[50, 50] 大小的图。
输入:
image: 4D 的 [N, H, W, C] 或 3D 的 [H, W, C] 数据。因此这个函数支持批处理。
offset_h, offset_w: 裁剪区域的左上角坐标
target_h, target_w: 输出区域的大小
输入:
image: 4D 的 [N, H, W, C] 或 3D 的 [H, W, C] 数据。因此这个函数支持批处理。
offset_h, offset_w: 原图上面要补充多少行0,原图左侧要补充多少列0
target_h, target_w: 输出区域的大小。多出的区域一律补0
tf.image.flip_up_down(image): 垂直翻转
tf.image.random_flip_up_down(image): 随机垂直翻转
tf.image.flip_left_right(image): 左右翻转
tf.image.random_flip_left_right(image): 随机左右翻转
tf.image.transpose_image(image): 沿对角线翻转。其实就是矩阵转置。
tf.image.rot90(image, k=1): 沿逆时针旋转 k 个 90 度。
以上函数均支持单个图像处理和批量处理。
说明:
(1) 以上前四个均有 random 函数,如: tf.image.random_brightness, tf.image.random_contrast, 具体参见 API。
(2) 在做一连串的调整操作后,图像的数值分布可能已经越界了。因此在最后一步操作后记得做有效截断:
result = tf.clip_by_value(adjusted_image, 0, 1)
输入:
image: 4D 的 [N, H, W, C] 图像。注意数据类型必须为 float。
boxes: [batch, box_num, 4]。bounding box 的坐标,四个点的坐标格式是 [y_min, x_min, y_max, x_max],这四个参数都是 [0, 1] 的数,表示比例。
输出: 带框的图像。
# img: [h, w, c]
batch_img = tf.expand_dims(img, 0) # [1, 400, 800, 3]
result = tf.image.draw_bounding_boxes(batch_img, boxes=[[[0.1, 0.3, 0.5, 0.7]]])
plt.imshow(result.eval()[0])
plt.show()
原图上框的位置: 左上角[240, 40],右下角[560, 200]
注意,框越界并不会报错。
输入:
image_size: [h, w, c] 原图的 shape。
bounding_boxes: [batch, box_num, 4], ground_truth 的位置信息。坐标格式仍然是 [y_min, x_min, y_max, x_max],四个参数均为 [0, 1] 之间的浮点数,表示比例。
min_object_covered: 默认 0.1。提取的区域至少包含某个 gt 标注框的比例的百分比。
aspect_ratio_range: list, 默认 [0.75, 1.33]。提取区域的宽高比的范围 (ratio = width / height)。
max_attempts: 默认100。最大尝试次数。
use_image_if_no_bounding_boxes: 默认 False。不提供标记框时是否返回原图。
输出:元组 (begin, size, bboxes):
begin: [offset_height, offset_width, 0]。输出区域起点,即左上角的坐标。
size: [target_height, target_width, -1]。输出区域的大小。
bboxes: 输出框的坐标,shape: [1, 1, 4]。
其中输出的 begin 和 size 可作为 tf.slice 的输入,bboxes 可作为 tf.image.draw_bounding_boxes 的输入。
# img: [h, w, c]
begin, size, bbox_for_draw = tf.image.sample_distorted_bounding_box(tf.shape(img),[[[0.1, 0.3, 0.5, 0.7]]], min_object_covered=0.4)
# distorted 为随机提取出来的图像区域
distorted = tf.slice(resized, begin, size)
**小坑注意:**上面的例子中,由于 tf.slice 收到的 size 中最后一个维度的数是 -1, 意思是多个通道全要提取,具体有几个通道是动态的(要根据输入来定),因此导致输出的 distorted 最后一个维度也是动态的。
print distorted.shape # (?, ?, ?)
result = tf.image.resize_images(distorted, [200, 200])
print result.shape # (200, 200, ?)
由于后面对 distorted 的操作一般不会涉及到图像通道数,为了图像的维度的 shape 能正常获取,最好在 tf.slice 后手动设定一下 shape:
# Restore the shape since the dynamic slice based upon the bbox_size loses
# the third dimension.
distorted.set_shape([None, None, 3])
print distorted.shape # (?, ?, 3)
result = tf.image.resize_images(distorted, [200, 200])
print result.shape # (200, 200, 3)
注意,以上操作均需要图片的数据类型为 tf.float32
这部分的官方代码在 slim/preprocessing/inception_preprocessing.py。
需要注意的两点:
(1) distorted_bounding_box_crop 函数的 min_object_covered 默认取 0.1,根据具体数据集调整这个参数比较合适。
(2) resize 都没有 align_corners.
import tensorflow as tf
from tensorflow.python.ops import control_flow_ops
def apply_with_random_selector(x, func, num_cases):
"""
从 func(x, 0), func(x, 1), ..., func(0, num_cases-1) 中随机选一个
"""
sel = tf.random_uniform([], maxval=num_cases, dtype=tf.int32)
# merge(inputs): 依次判断 inputs 中的元素是否存在, 返回第一个存在的元素的[data, data_index]
# switch(data, pred): 返回 (output_false, output_true), 如果 pred 为 true, output_true = data, output_false
# 不存在; 反过来也一样
return control_flow_ops.merge([
func(control_flow_ops.switch(x, tf.equal(sel, case))[1], case)
for case in range(num_cases)])[0]
def distort_color(image, color_ordering=0, fast_mode=True, scope=None):
"""
对图片进行随机色彩变换:调整亮度、饱和度、色相、对比度。
Args:
image (float): [0, 1] 之间的 3D tensor 图像
color_ordering (int, optional): Defaults to 0. 可取 0-3,代表不同的随机变换模式。
fast_mode (bool, optional): Defaults to True. 快速模式下不采用调整色相和对比度的变换。
scope (optional): Defaults to None.
Returns:
颜色变换处理后的 float32 图像。
"""
with tf.name_scope(scope, 'distort_color', [image]):
if fast_mode:
if color_ordering == 0:
image = tf.image.random_brightness(image, max_delta=32. / 255.)
image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
else:
image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
image = tf.image.random_brightness(image, max_delta=32. / 255.)
else:
if color_ordering == 0:
image = tf.image.random_brightness(image, max_delta=32. / 255.)
image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
image = tf.image.random_hue(image, max_delta=0.2)
image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
elif color_ordering == 1:
image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
image = tf.image.random_brightness(image, max_delta=32. / 255.)
image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
image = tf.image.random_hue(image, max_delta=0.2)
elif color_ordering == 2:
image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
image = tf.image.random_hue(image, max_delta=0.2)
image = tf.image.random_brightness(image, max_delta=32. / 255.)
image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
elif color_ordering == 3:
image = tf.image.random_hue(image, max_delta=0.2)
image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
image = tf.image.random_brightness(image, max_delta=32. / 255.)
else:
raise ValueError('color_ordering must be in [0, 3]')
# 有效截断
return tf.clip_by_value(image, 0.0, 1.0)
# min_object_covered 默认为 0.1,可以根据实际任务稍微改大一点
def distorted_bounding_box_crop(image,
bbox,
min_object_covered=0.1,
aspect_ratio_range=(0.75, 1.33),
area_range=(0.05, 1.0),
max_attempts=100,
scope=None):
with tf.name_scope(scope, 'distorted_bounding_box_crop', [image, bbox]):
sample_distorted_bounding_box = tf.image.sample_distorted_bounding_box(
tf.shape(image),
bounding_boxes=bbox,
min_object_covered=min_object_covered,
aspect_ratio_range=aspect_ratio_range,
area_range=area_range,
max_attempts=max_attempts,
use_image_if_no_bounding_boxes=True)
bbox_begin, bbox_size, distort_bbox = sample_distorted_bounding_box
cropped_image = tf.slice(image, bbox_begin, bbox_size)
return cropped_image, distort_bbox
def preprocess_for_train(image, height, width, bbox,
fast_mode=True,
scope=None,
add_image_summaries=True):
"""
训练集数据预处理。
Args:
image: 输入图像, uint8 或者 float32(都会被转换为 float32). shape: [H, W, C]
bbox: shape: [1, num_boxes, coords]. coords: [ymin, xmin, ymax, xmax]. 为 None 时取原图整图.
fast_mode: resize 插值和颜色变换是否采用快速模式。
add_image_summaries: 是否画出画出中间处理过程得到的图。
Returns:
3D float 图像。范围在 [-1, 1] 之间。
"""
with tf.name_scope(scope, 'distort_image', [image, height, width, bbox]):
# bbox 为空取原图
if bbox is None:
bbox = tf.constant([0.0, 0.0, 1.0, 1.0],
dtype=tf.float32,
shape=[1, 1, 4])
if image.dtype != tf.float32:
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
image_with_box = tf.image.draw_bounding_boxes(tf.expand_dims(image, 0),
bbox)
if add_image_summaries:
tf.summary.image('image_with_bounding_boxes', image_with_box)
distorted_image, distorted_bbox = distorted_bounding_box_crop(image, bbox)
# Restore the shape since the dynamic slice based upon the bbox_size loses
# the third dimension.
distorted_image.set_shape([None, None, 3])
image_with_distorted_box = tf.image.draw_bounding_boxes(
tf.expand_dims(image, 0), distorted_bbox)
if add_image_summaries:
tf.summary.image('images_with_distorted_bounding_box',
image_with_distorted_box)
# 快速模式下: resize 采取双线性插值,否则是选择随机方法插值。
num_resize_cases = 1 if fast_mode else 4
distorted_image = apply_with_random_selector(
distorted_image,
lambda x, method: tf.image.resize_images(x, [height, width], method),
num_cases=num_resize_cases)
if add_image_summaries:
tf.summary.image('cropped_resized_image',
tf.expand_dims(distorted_image, 0))
# 随机左右翻转
distorted_image = tf.image.random_flip_left_right(distorted_image)
# 调用 distort_color 随机做颜色变换,选择是否采用快速模式。
num_distort_cases = 1 if fast_mode else 4
distorted_image = apply_with_random_selector(
distorted_image,
lambda x, ordering: distort_color(x, ordering, fast_mode),
num_cases=num_distort_cases)
if add_image_summaries:
tf.summary.image('final_distorted_image',
tf.expand_dims(distorted_image, 0))
# [0, 1] 之间的图,变为 [-1, 1] 之间。
distorted_image = tf.subtract(distorted_image, 0.5)
distorted_image = tf.multiply(distorted_image, 2.0)
return distorted_image
def preprocess_for_eval(image, height, width,
central_fraction=0.875, scope=None):
"""
验证集数据预处理。做中心裁剪,双线性插值 resize,数值范围变换到 [-1, 1] 之间。
Returns:
3D float 图像。范围在 [-1, 1] 之间。
"""
with tf.name_scope(scope, 'eval_image', [image, height, width]):
if image.dtype != tf.float32:
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
# 取出中间 0.875 的区域
if central_fraction:
image = tf.image.central_crop(image, central_fraction=central_fraction)
if height and width:
# resize 图像,采用双线性插值
image = tf.expand_dims(image, 0)
image = tf.image.resize_bilinear(image, [height, width],
align_corners=False)
image = tf.squeeze(image, [0])
# 取值范围变为 [-1, 1] 之间
image = tf.subtract(image, 0.5)
image = tf.multiply(image, 2.0)
return image
def preprocess_image(image, height, width,
is_training=False,
bbox=None,
fast_mode=True,
add_image_summaries=True):
"""
训练集和验证集图像预处理。
Args:
image: 输入图像, uint8 或者 float32(都会被转换为 float32). shape: [H, W, C]
is_training: 训练集还是测试集
bbox: 标记框,默认 None 表示取原图整图
fast_mode: resize 和颜色变换是否采用快速模式
add_image_summaries: 中间处理后的图是否画图。
Returns:
3D float 图像。范围在 [-1, 1] 之间。
"""
if is_training:
return preprocess_for_train(image, height, width, bbox, fast_mode,
add_image_summaries=add_image_summaries)
else:
return preprocess_for_eval(image, height, width)
其他的如 VGG 预处理,可参见: vgg_preprocessing.py,VGG 裁剪图片时是确定短边长度后再等比例 resize.
TensorFlow 中提供的队列有:
这些类型的 Queue 的 API 大致差不多,以 FIFOQueue 为例:
tf.FIFOQueue(capacity, dtypes, shapes=None): capacity 为队列容量,dtypes 为队列中元素的数据类型,shapes 为队列中元素的 shape。
-> 常用函数:
- dequeue(): 出队一个元素
- dequeue_many(n): 出队 n 个元素。使用此函数必须手动指定 shapes。
- enqueue(): 入队一个元素
- enqueue_many(val): 入队多个元素. 注意这里 val 要比基元多一维。
- size(): 返回队列中元素多少。返回数据类型为 tensor。
注意: 队列中存储了 capacity 个基元,以 list 形式存在。当基元中包含多个数据时,dtypes 是个 list,长度与基元长度要相同。若要使用 dequeue_many(n),shape 必须手动指定。
q = tf.FIFOQueue(5, tf.int32, shapes=[()])
op1 = q.enqueue_many([[1, 2]])
op1.run()
op2 = q.enqueue([3])
op2.run()
print q.size().eval() # 3
print q.dequeue().eval() # 1
print q.dequeue_many(2).eval() # [2, 3]
TODO(20180817): 当基元有多个元素时,enqueue_many 表现很奇怪,尚未弄懂。因此暂时避免使用 enqueue_many 和 dequeue_many。
该类的常用方法:
使用方法:
try:
coord = tf.train.Coordinator()
... codes of creating threads ...
coord.join(threads)
except Exception as e:
... some codes ...
上面创建线程的代码为:
try:
while not coord.should_stop():
... some work ...
except Exception as e:
coord.request_stop(e)
coord.join(threads)
可以用 stop_on_exception () 简化上面创建线程的代码:
with coord.stop_on_exception():
while not coord.should_stop():
... some work ...
coord.join(threads)
tf 中的多线程使用的队列启动方案。与 tf.train.Coordinator 一起使用。
比如一个经典的文件输入流程: 第一批线程通过往第一个队列里面不断填充要处理的文件名;第二批线程从前面的队列取出文件名,然后进行读取处理等操作,得到的张量放在第二个队列;第三批线程从第二个队列中取出张量,组成 batch,输入网络进行训练。
初始化方法:
__init__(queue=None, enqueue_ops=None)
其中 queue 为要操作的队列,enqueue_ops 为要对该队列执行的多线程操作。
tf.train.QueueRunner 常与以下两个类一起使用:
一个完整的例子:
# 创建队列,入队操作
queue = tf.FIFOQueue(100, tf.float32)
enqueue_op = queue.enqueue([tf.random_normal([])])
# 开启 5 个线程
qr = tf.train.QueueRunner(queue, [enqueue_op] * 5)
tf.train.add_queue_runner(qr)
out_tensor = queue.dequeue()
with tf.Session() as sess:
coord = tf.train.Coordinator()
enqueue_threads = tf.train.start_queue_runners(sess=sess, coord=coord)
for i in range(5):
print sess.run(out_tensor)
# 终止所有线程
coord.request_stop()
coord.join(enqueue_threads)
当然,上面也可以不使用 tf.train.add_queue_runner 和 tf.train.start_queue_runner 来启动线程。直接用 QueueRunner 的 create_threads(sess, coord=None, daemon=False, start=False) 来启动多个线程:
queue = tf.FIFOQueue(100, tf.float32)
enqueue_op = queue.enqueue([tf.random_normal([])])
# 开启 5 个线程
qr = tf.train.QueueRunner(queue, [enqueue_op] * 5)
out_tensor = queue.dequeue()
with tf.Session() as sess:
coord = tf.train.Coordinator()
# 手动启动 qr 负责的入队线程
enqueue_threads = qr.create_threads(sess=sess, coord=coord, start=True)
for i in range(5):
print sess.run(out_tensor)
coord.request_stop()
coord.join(enqueue_threads)
tf.train.match_filenames_once(pattern): 根据正则表达式获取符合要求的文件名列表。返回一个局部变量,为文件名列表。因此使用这和函数务必初始化局部变量。
tf.train.string_input_producer(string_tensor, num_epochs=None, shuffle=True, capacity=32): 根据文件名返回一个文件名队列,供多线程使用。string_tensor: 装有文件名的 tensor,可由 match_filenames_onces 函数返回,也可用 Python glob 生成等;num_epochs: 加载文件列表的最大轮数,默认无限循环;shuffle: 文件名加入队列前是否打乱;capacity: 队列长度。
注意: 从源码中发现,该函数创建了局部变量(对 epoch 进行计数),因此使用时要初始化局部变量(其实不初始化也可以,就不使用 epoch 这个变量)。该函数返回一个 FIFOQueue 用于存放文件名,并生成一个 QueueRunner 进行单线程入队操作,该 QueueRunner 放在默认的 tf.GraphKeys.QUEUE_RUNNERS 这个 collection 中。
在指定 num_epoches (如测试时指定为 1 ) 时,队列为空后继续出队,抛出 OutOfRange 异常。
sess = tf.InteractiveSession()
# 获取所有 png 图片文件列表
files = tf.train.match_filenames_once('some_path/*.png')
filename_queue = tf.train.string_input_producer(files, shuffle=False)
coord = tf.train.Coordinator()
thread = tf.train.start_queue_runners(sess, coord)
tf.local_variables_initializer().run()
print filename_queue.dequeue().eval() # 获取一个文件名
前面 tf.train.string_input_producer 生成了文件名队列,tf 通过各种 Reader 从这个文件名队列中取文件名,进行文件读取解析。常用的 Reader 有:
它们的 API 大致相同。常用的方法有:
用 Reader 实现 2.1 节的图片读取:
files = tf.train.match_filenames_once('some_path/*.png')
filename_queue = tf.train.string_input_producer(files, shuffle=False)
key, value = tf.WholeFileReader().read(filename_queue)
image = tf.image.decode_png(value, channels=3)
with tf.Session() as sess:
sess.run(tf.local_variables_initializer())
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
print sess.run(image)
单线程,效率较低。
一个高效的数据 pipeline 应该是一个生产者——消费者模型,即生产者是一个文件名队列,里面存储要处理的文件的名字,这个队列用单线程处理即可;消费者为多线程从生产者队列里面取出文件名,进行文件读取和预处理,然后放置在另一个队列,最终的数据从这个队列中取出。
一个完整的流程如图:
TensorFlow 中提供的对应的 batch 数据的函数为:
注意: 以上 dynamic_pad 为 False 时,传入的 tensors 必须显式确定,否则抛出异常。
一般我们的文件不止一个,比如 TensorFlow Performance Guide 建议,把大数据文件分割成多个约为 100 MB 的 TFRecord 文件,I/O 性能比较好。这种情况下,多文件,多线程进行读取和预处理操作应该用上面两个函数。
其中,输入的 tensors_list 为 a list of tuples of tensors,创建 len(tensors_list) 个线程,每个线程读取一个文件,然后压入队列:
# features 为解析的 TFRecord 文件
image, label = features['image'], features['label']
# 1. 使用 tf.train.batch:
# 返回的 iamge 是个 [N, H, W, C] 的 tensor
# 记住 image 和 label 要一起 run,不然就错位交叉了
image, label = tf.train.batch([image, label], batch_size=10, num_threads=1, capacity=100)
# 2. 使用 tf.train.batch_join:
image, label = tf.train.batch_join([[image, label] for _ in range(4)], batch_size=10, capacity=100)
比较: tf.train.batch 是多线程读取一个文件,tf.train.batch_join 是多线程读取多个文件,每个线程负责一个文件。如果同一个文件中样本相似,用 tf.train.batch 显然不合适;使用 tf.train.batch_join() 时,如果线程数大于文件数,那么也存在多个线程读取同一个文件的情况,而且多线程读多个文件的硬盘寻址也是有时间开销的,可能会让效率变低。
# coding: utf-8
import tensorflow as tf
files = tf.train.match_filenames_ones('some_path/data.tfrecords-*')
# 训练集,文件名打乱
filename_queue = tf.train.string_input_producer(files, shuffle=True, )
_, serialized_example = tf.TFRecordReader().read(filename_queue)
features = tf.parse_single_example(serialized_example, features={
'image': tf.FixedLenFeature([], tf.string),
'label': tf.FixedLenFeature([], tf.int64),
'height': tf.FixedLenFeature([], tf.int64),
'width': tf.FixedLenFeature([], tf.int64),
'channels': tf.FixedLenFeature([], tf.int64)
})
image, label, height, width, channels = features['image'], features['label'], features['height'], features['width'], features['channels']
decoded_image = tf.decode_raw(image, tf.uint8)
decoded_image = tf.reshape(decoded_image, tf.stack([height, width, channels]))
image_size = 299
# 前面的 inception 预处理代码
distorted_image = preprocessing_for_train(decoded_image, image_size, image_size, None)
# 这一部分可以具体调整
num_threads = 10
batch_size = 50
min_after_dequeue = 1000
# as suggested by https://www.tensorflow.org/api_guides/python/reading_data#Batching
capacity = min_after_dequeue + (num_threads + 10) * batch_size
image_batch, label_batch = tf.train.shuffle_batch([distorted_image, label], batch_size=batch_size,
capacity=capacity, min_after_dequeue=min_after_dequeue, num_threads=num_threads)
# tf.train.shuffle_batch_join 的方案
# image_batch, label_batch = tf.train.shuffle_batch([[distorted_image, label] for i in range(num_threads)], batch_size=batch_size,
# capacity=capacity, min_after_dequeue=min_after_dequeue)
logit = inference(image_batch)
loss = calc_loss(logit, label_batch)
train_step = ...
with tf.Session() as sess:
sess.run(tf.global_variables_initializer(), tf.local_variables_initializer())
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
with coord.stop_on_exception():
while not coord.should_stop():
for i in range(training_epoches):
sess.run(train_step)
coord.join(threads)
tf 1.4 后的数据输入框架,抛弃队列处理的旧 API,使用 Dataset 数据集提供数据的输入。这一部分比较简单,官方文档很详细。
(1) 利用数据集的基本步骤:
(2) 常用的 Dataset:
(3) Dataset 常用的属性:
(4) Dataset 常用的方法:
batch(batch_size): 按 batch_size 取一个 batch。最后不够一个 batch_size 也会被取出来,如果不要最后的零头,使用 tf.contrib.data.batch_and_drop_remainder 方法。
padded_batch(batch_size, padded_shapes, padding_values=None): 生成 padded batch。
shuffle(buffer_size, seed=None, reshuffle_each_iteration=None): 打乱数据集。buffer_size: 缓冲区大小。默认 reshuffle_each_iteration 为 True。因此,每迭代一个元素出来,缓冲区中填充一个新元素,shuffle 一次。buffer_size 越大,打乱性能越好,但是第一次启动的时间就较长。可设置 buffer_size 为数据集大小,这样打乱充分。参考链接。
repeat(count=None): 把 Dataset 重复 count 次。None 或 -1 表示无限循环。
skip(count): 跳过前 count 个元素。
take(count): 读取前 count 个元素。
shard(num_shards, index): 生成一个只包含原 Dataset 的 1/num_shards 的新 Dataset。
from_tensors(tensors): 根据单个元素构建 Dataset。
from_tensor_slices(tensors): 根据元素切片构建 Dataset。
from_generator(generator, output_types, out_shapes=None): 根据生成器构建 Dataset。这一部分使用 tf.py_func 实现的。
map(map_func, num_parallel_calls=None): map 运算,返回一个 map 运算后的 Dataset。
flat_map(map_func): map_func 后再 flat 展平,返回一个 Dataset。
filter(predicate): filter 运算,返回一个 Dataset。
interleave(map_func, cycle_length, block_length=1): 适用于分布式文件系统。当有多个文件时,一次对 cycle_length 个文件同时读取,block_length 是每个线程输出元素的个数。因此,map 和 flat_map 相当于 tf.train.batch,interleave 相当于 tf.train.batch_join。
apply(transformation_func): 对一个 Dataset 进行某个操作,类似于 map。
zip(datasets): 和 Python 中的 zip 函数一样,把多个 Dataset zip起来。
concatenate(dataset): 串接一个 Dataset, 返回一个新的合并的 Dataset。
prefetch(buffer_size): 预加载 buffer_size 的数据。
range(*args): 生成一个 RangeDataset。
list_files(file_pattern, shuffle=None): 根据文件名 file_pattern 正则表达式,获取一个包含这些文件的 Dataset。注意,这个顺序是不定的,即使 shuffle 为 False。看源码发现这个函数其实是先用 tf.matching_files 得到文件列表,在用 from_tensor_slices 得到一个 dataset,最后 shuffle。注意,最后一个 shuffle 默认的缓冲区为整个文件名列表。因此,由于 from_tensor_slices 和 shuffle 缓冲区长度的设定,当文件名列表过于巨大时,这一步耗时就会很大,可参考 Github。
(5)迭代器:
以一个简单的文本处理为例,假设为分布式文件系统,有 5 个 txt 文件,每个文件里面存放某一类样本的文件名和类别标号,形如:
- file1.txt:
cat1.jpg 0
cat2.jpg 0
...
cat4.jpg 0
- file2.txt
dog1.jpg 1
dog2.jpg 1
...
dog4.jpg 1
...
- file_n.txt
(1) 基本流程,无各种优化考虑:
# 获取文件列表,并按照 file_{n} 中的数字 n 从小到大排序
txt_files = glob.glob('some_path/*.txt')
txt_files.sort(key=lambda x: int(x.split('.')[-2][-1]))
# 使用 TextLineDataset 读取文本文件
dataset = tf.data.TextLineDataset(txt_files)
dataset = dataset.shuffle(buffer_size=20)
dataset = dataset.repeat(2)
# 分割字符串,按空格分割
dataset = dataset.map(lambda x: tf.string_split([x], delimiter=' ').values)
dataset = dataset.batch(2)
# 创建迭代器
iterator = dataset.make_one_shot_iterator()
next_element = iterator.get_next()
输出的结果为:
[['bird1.jpg' '3']
['dog4.jpg' '1']]
[['dog2.jpg' '1']
['bird3.jpg' '3']]
...
要注意的几点:
-- 参考 4.1 中设置 shuffle 的 buffer_size
-- shuffle 和 repeat 的顺序,建议先 shuffle 再 repeat,如果反过来会造成打乱效果变差。参考: Dataset Performance Guide。
(2) 基础性能优化: 多线程读取多文件,多线程 map,预加载 prefetch
上面代码的性能缺点:单线程读取,单线程 map,加上 Dataset 默认的 lazy 属性,性能地下。
基础改进代码:
txt_files = glob.glob('some_path/*.txt')
txt_files.sort(key=lambda x: int(x.split('.')[-2][-1]))
# 改进1:多线程读取文件,创建5个线程,每个线程负责一个文件读取。
# 因此改进后可以同时读 5 个文件
dataset = tf.data.Dataset.from_tensor_slices(txt_files)
dataset = dataset.interleave(lambda x: tf.data.TextLineDataset(x),
cycle_length=5, block_length=1)
dataset = dataset.shuffle(buffer_size=20)
dataset = dataset.repeat(2)
# 改进2: 多线程 map 函数,也就是可以多线程预处理了。
dataset = dataset.map(lambda x: tf.string_split([x], delimiter=' ').values,
num_parallel_calls=4)
dataset = dataset.batch(2)
# 改进3:预加载,输出有缓冲区。类似于消费者队列中填充一定 batch 数,等待消费。
# 这里是基于前一步操作后的元素,因此是预加载 5 个 batch。
dataset = dataset.prefetch(5)
...
TODO: 上面的 interleave 同时读取 5 个文件,但是真的是并行吗?按照 tf.data API slides 应该是并行 I/O,但是按照 Dataset Performance Guide 却建议使用 tf.contrib.data.parallel_interleave() 实现真正的并行 I/O,这里有待进一步明确,是否需要改成下面的版本:
dataset = dataset.apply(tf.contrib.data.parallel_interleave(tf.data.TextLineDataset, cycle_length=4))
(3) 进一步优化:
并行化 batch: 当 batch_size 比较大时,取 batch 也是耗时的,因此可以把 map 和 batch 放在一起做多线程,最终改成这样的版本(使用 tf.contrib.data.map_and_batch 函数):
txt_files = glob.glob('some_path/*.txt')
txt_files.sort(key=lambda x: int(x.split('.')[-2][-1]))
# 确保运行在 CPU 上
with tf.device('/cpu:0'):
dataset = tf.data.Dataset.from_tensor_slices(txt_files)
dataset = dataset.apply(tf.contrib.data.parallel_interleave(tf.data.TextLineDataset, cycle_length=4))
dataset = dataset.shuffle(buffer_size=20)
dataset = dataset.repeat(2)
# 改动这里
dataset = dataset.apply(tf.contrib.data.map_and_batch(lambda x: tf.string_split([x], delimiter=' ').values, batch_size=2, num_parallel_batches=4))
dataset = dataset.batch(2)
dataset = dataset.prefetch(5)
其他的优化还有内存 cache 等的考虑等,参考 Dataset Performance Guide 。
注意:一般而言, batch 耗时相对较少,当电脑核心不太够的时候,并行化的 batch 占据线程也不一定是好事,可能速度也会变慢。
(1) initializable iterator: 动态指定 iterator 参数。配合 placeholder 使用。
# 指定文件名的 placeholder
txt_files = tf.placeholder(tf.string, [])
dataset = tf.data.TextLineDataset([txt_files])
dataset = dataset.map(lambda x: tf.string_split([x], delimiter=' ').values)
iterator = dataset.make_initializable_iterator()
next_element = iterator.get_next()
iterator.initializer.run(feed_dict={txt_files: './txt_files/file1.txt'})
for i in range(5):
print next_element.eval()
# 切换到另一个 txt 文件
iterator.initializer.run(feed_dict={txt_files: './txt_files/file2.txt'})
for i in range(2):
print next_element.eval()
# 中间重新初始化,迭代器从头开始
iterator.initializer.run(feed_dict={txt_files: './txt_files/file2.txt'})
for i in range(5):
print next_element.eval()
(2) reinitializable iterator: 一个可重复初始化的迭代器,绑定到不同的数据集上去。
# 两个数据集
cat_dataset = tf.data.TextLineDataset(['./txt_files/file1.txt']).map(lambda x: tf.string_split([x], delimiter=' ').values)
dog_dataset = tf.data.TextLineDataset(['./txt_files/file2.txt']).map(lambda x: tf.string_split([x], delimiter=' ').values)
# 同一个 reinitializable iterator
iterator = tf.data.Iterator.from_structure(cat_dataset.output_types, dog_dataset._output_shapes)
next_element = iterator.get_next()
# 迭代器初始化
cat_init_op = iterator.make_initializer(cat_dataset)
dog_init_op = iterator.make_initializer(dog_dataset)
# 迭代器绑定到 cat dataset
cat_init_op.run()
for _ in range(5):
print next_element.eval()
# 迭代器绑定到 dog dataset
dog_init_op.run()
for _ in range(5):
print next_element.eval()
通常可以用同一个迭代器绑定到 training set 和 test_set,不过,用前面的 initializer_iterator 也可实现相同功能。
(3) feedable iterator: 迭代器是可变的,目的是通过选择不同数据集的 Dataset 的迭代器来迭代不同的数据。
# 创建两个 Dataset
cat_dataset = tf.data.TextLineDataset(['./txt_files/file1.txt']).map(lambda x: tf.string_split([x], delimiter=' ').values)
dog_dataset = tf.data.TextLineDataset(['./txt_files/file2.txt']).map(lambda x: tf.string_split([x], delimiter=' ').values)
# 创建两个 Dataset 对应的迭代器
cat_iterator = cat_dataset.make_one_shot_iterator()
dog_iterator = dog_dataset.make_initializable_iterator()
# 创建代表两个 Dataset 的迭代器的 handle tensor
cat_handle = cat_iterator.string_handle().eval()
dog_handle = dog_iterator.string_handle().eval()
# 根据传入的 handle 选择具体的迭代器
handle = tf.placeholder(tf.string, [])
iterator = tf.data.Iterator.from_string_handle(handle, cat_dataset.output_types, cat_dataset.output_shapes)
next_element = iterator.get_next()
while True:
# 这里切换数据集,之后再切回 cat_dataset 是继续之前的数据迭代。
for _ in range(2):
print next_element.eval(feed_dict={handle: cat_handle})
dog_iterator.initializer.run()
for _ in range(2):
print next_element.eval(feed_dict={handle: dog_handle})
reinitializable iterator 和 feedable iterator 的区别:
二者都是实现切换数据集的功能,但是 reinitializable iterator 是同一个迭代器绑定到不同数据集,切换数据集的时候要重新绑定,然后初始化这个迭代器,特点是切换数据集后从头开始迭代切换到的新数据集;
feedable iterator 是针对每个数据集先建立对应的迭代器,然后选择用哪一个迭代器来选择对应的数据集,如果这些迭代器在创建的时候先初始化好,那么在迭代器之间来回切换的时候,**各自的数据集是接着前一次的时间点继续输出的。**比如训练集特别大,并不想训练集完全迭代一轮再验证,迭代一定数量的较少样本后就想测试一次验证集,就可以用这种迭代器。
(1) 读取 npy 文件:
可以直接用 from_tensor_slices 一次载入:
data = np.load('data.npy')
features = data["features"]
labels = data["labels"]
dataset = tf.data.Dataset.from_tensor_slices((features, labels))
...
缺点是:整个 npy 解析出来的数组以 tf.constant() 的形式存在于 Graph 中,而一个 Graph protobuf 文件的上限大小为 2G。
可以使用 initializable_iterator 配合 placeholder 载入:
data = np.load('data.npy')
features = data["features"]
labels = data["labels"]
# 创建对应的 placeholder
features_placeholder = tf.placeholder(features.dtype, features.shape)
labels_placeholder = tf.placeholder(labels.dtype, labels.shape)
dataset = tf.data.Dataset.from_tensor_slices((features_placeholder, labels_placeholder))
... 一些其他预处理操作等 ...
iterator = dataset.make_initializable_iterator()
sess.run(iterator.initializer, feed_dict={feature_placeholder: features, labels_placeholder: labels})
上面方案其实也不好。上面两个是官方提供的方案。
Stack Overflow 上提供了其他方案:
比如这个方案计算出 npy 的 header_bytes 长度,然后用 tf.data.FixedLengthRecordDataset 来解析 npy 二进制文件。如果要并行处理,那么多个 npy 的 header_bytes 长度要一样。
另外,可以用 tf.py_func 来调用 np.load() 函数,然后放到 Dataset.map() 函数里面解决:
file_list = ['a.npy', 'b.npy', ...]
def read_npy_file(file):
return np.load(file).as_type(np.float32)
dataset = tf.data.Dataset.from_tensor_slices(file_list)
dataset = dataset.map(lambda file: tuple(tf.py_func(
read_npy_file, [file], [tf.float32])))
...
其他的一些应用可以参考 Tensorflow Dataset Guide -- Importing Data,里面有从 Python 迭代器得到 Dataset,读取 csv 等文件,进行 padding 操作,利用 tf.py_func 调用 opencv 等。
用 tf.data 重写 3.2.6 部分:
import tensorflow as tf
IMAGE_SIZE = 299 # 输入图片大小
BATCH_SIZE = 50
SHUFFLE_BUFFER = 10000
NUM_EPOCHES = 100
# 列举 tfrecord 文件名
training_files = tf.train.match_filenames_ones('training_path/data.tfrecords-*')
val_files = tf.train.match_filenames_ones('val_path/data.tfrecords-*')
def tfrecord_parser(record):
features = tf.parse_single_example(
record, features={
'image': tf.FixedLenFeature([], tf.string),
'label': tf.FixedLenFeature([], tf.int64),
'height': tf.FixedLenFeature([], tf.int64),
'width': tf.FixedLenFeature([], tf.int64),
'channels': tf.FixedLenFeature([], tf.int64)
}
)
label, height, width, channels = features['label'], features['height'], features['width'], features['channels']
decoded_image = tf.decode_raw(features['image'], tf.uint8)
decoded_image = tf.reshape(decoded_image, tf.stack([height, width, channels]))
return decoded_image, label
# 定义 traing dataset 和对应的迭代器
# dataset = tf.data.TFRecordDataset(training_files) 这个效率低
training_dataset = tf.data.Dataset.from_tensor_slices(training_files)
training_dataset = training_dataset.interleave(lambda x: tf.data.TFRecordDataset(x), cycle_length=4)
training_dataset = training_dataset.shuffle(SHUFFLE_BUFFER)
training_dataset = training_dataset.repeat(NUM_EPOCHES)
training_dataset = training_dataset.map(tfrecord_parser, num_parallel_calls=2)
training_dataset = training_dataset.map(lambda image, label: (preprocess_for_train(image, IMAGE_SIZE, IMAGE_SIZE), label), num_parallel_calls=4)
training_dataset = training_dataset.batch(BATCH_SIZE)
training_dataset = training_dataset.prefetch(5)
training_iterator = training_datasetmake_initializable_iterator()
training_image_batch, training_label_batch = training_iterator.get_next()
# 网络训练部分
logit = inference(training_image_batch)
loss = calc_loss(logit, training_label_batch)
train_step = ...
# 创建 validation dataset 和对应的迭代器
val_dataset = tf.data.TFRecordDataset(val_files)
val_dataset = val_dataset.map(tfrecord_parser)
val_dataset = val_dataset.map(lambda image, label: (preprocess_for_val(image, IMAGE_SIZE, IMAGE_SIZE), label), num_parallel_calls=2)
val_dataset = val_dataset.batch(BATCH_SIZE)
val_iterator = val_dataset.make_initializable_iterator()
val_image_batch, val_label_batch = val_iterator.get_next()
# 网络测试部分
val_logit = inference(val_image_batch)
predictions = tf.argmax(val_logit, axis=-1, output_type=tf.int32)
with tf.Session() as sess:
sess.run(tf.global_variables_initializer(), tf.local_variables_initializer())
sess.run(training_iterator.initializer)
while True:
try:
sess.run(train_step)
except tf.errors.OutOfRangeError:
break
sess.run(val_iterator.initializer)
val_results = []
val_labels = []
while True:
try:
pred, label = sess.run([predictions, val_label_batch])
val_results.extend([pred])
val_labels.extend([label])
except tf.errors.OutOfRangeError:
break
# 计算准确率等指标
correct = [float(y == y_) for (y, y_) in zip(val_results, val_labels)]
acc = sum(correct) / len(correct)
]]>A
对应的二进制数值为01000001
,对应的十进制数就是65。GB2312
,又称GB0,共收录了6763个汉字,兼容ASCII。后来扩展到GBK
,收录了27484个汉字,同时还收录了藏文等少数名族文字。GBK也是兼容ASCII,英文字符用1个字节表示,汉字用两个字节表示。字节与字符:
编码与解码
Python2默认用ASCII编码:
import sys
print sys.getdefaultencoding() # ascii
就是说默认的解释器会把str类型的字符串当做ASCII编码来处理。
注意区分的是:文档开头往往加上一句# coding: utf-8
是来指定脚本文件的编码方式。
Python2中,str和unicode都是basestring的子类,可见str和unicode是两种不同类型的字符串对象。
以汉字“禅”为例,str打印出来就是十六进制形式的\xe7\xa6\x85,对应于一长串二进制序列;而用unicode打印出来就是unicode符号u'\u7985':
>>> s = '禅'
>>> s
'\xe7\xa6\x85'
>>> type(s)
<type 'str'>
>>> u = u'禅'
>>> u
u'\u7985'
>>> type(u)
<type 'unicode'>
如果要把unicode符号保存到文件,或者是传输到网络,那就必须编码为str类型;反之亦然:
>>> u = u'禅'
>>> u
u'\u7985'
>>> u.encode('utf-8')
'\xe7\xa6\x85'
>>> s = '禅'
>>> s
'\xe7\xa6\x85'
>>> s.decode('utf-8')
u'\u7985'
说白了:
编码:字符到二进制数据的转换,即: encode: unicode → str
解码:二进制数据到字符的转换,即: decode: str → unicode
不同的编码之间通过unicode作为中间媒介来互相转换:
比如一个用utf-8编码好的汉字'\xe7\xa6\x85',要变成gbk:
'\xe7\xa6\x85'.decode('utf-8').encode('gbk')
case1:
>>> s = '你好'
>>> s.decode()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
s.decode()默认使用ascii解码,但是ascii字符集中是没有中文字符的,因此报错: 0xe4超出范围了
case2:
>>> a = u'你好'
>>> a.encode()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
与case1类似,unicode转换str时默认用ascii,也是报错。
case3:
>>> s = '你好' # str类型
>>> y = u'python' # unicode类型
>>> s + y # 隐式转换,即 s.decode('ascii') + u
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
str和unicode混用时,str会隐式地decode为unicode,然后也是由于用了ascii找不到汉字报错。这里详细见3.8节。
# coding: utf-8
>>> a='好'
>>> a
'\xe5\xa5\xbd'
>>> b=a.decode("utf-8")
>>> b
u'\u597d'
>>> c=b.encode("gbk")
>>> c
'\xba\xc3'
>>> print c
��
utf-8编码的字符‘好’占用3个字节,解码成Unicode后,如果再用gbk来解码后,只有2个字节的长度了,最后出现了乱码的问题,因此防止乱码的最好方式就是始终坚持使用同一种编码格式对字符进行编码和解码操作。
str()和unicode()是两个工厂方法,分别返回str字符串对象和unicode字符串对象。
其实本质为:
str(s) = s.encode('ascii')
unicode(s) = s.decode('ascii')
>>> s3 = u"你好"
>>> s3
u'\u4f60\u597d'
>>> str(s3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
对于一个unicode形式的str字符串,如\u4f60\u597d
这样的,如何变成真正的unicode呢?
最简单是前面加个u:
>>> print u'\u4f60\u597d'
你好
但是在像解析html时,储存在了一个字符串中,就可以这么做:
>>> s='\u4f60\u597d'
>>> type(s)
<type 'str'>
>>> s = s.decode('unicode-escape')
>>> print s
你好
可用chardet检测字符编码:
>>> import chardet
>>> a = '好'
>>> print chardet.detect(a)
{'confidence': 0.73, 'language': '', 'encoding': 'ISO-8859-1'}
Python2会在必要的情况下,对string作必要的编码类型转换,如==
操作,字节和字符拼接,以及对str编码(encode)时。
看3.4的例子,我们这么操作就不会报错了:
>>> import sys
>>> reload(sys)
<module 'sys' (built-in)>
>>> sys.setdefaultencoding('utf-8') # 初始化后删除了 sys.setdefaultencoding 方法,我们需要重新载入
>>> s = '你好'
>>> y = u'python'
>>> s + y
u'\u4f60\u597dpython'
s和y类型不一样,于是Python调用更改后的默认编码utf-8对s进行decode为unicode再操作。
再看:
>>> s='你好'
>>> s.encode('gb2312')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
直接对str进行编码,首先会解码到unicode,默认ASCII,所以报错。
修改的方式为改成s.decode('utf-8').encode('gb2312')
或者加上sys.setdefaultencoding('utf-8')
注意: 用这种方法是有潜在危害的,详见: 立即停止使用 setdefaultencoding('utf-8'), 以及为什么
文中提到,好的习惯是:
- 所有 text string 都应该是 unicode 类型,而不是 str,如果你在操作 text,而类型却是 str,那就是在制造 bug。
- 在需要转换的时候,显式转换。从字节解码成文本,用 var.decode(encoding),从文本编码成字节,用 var.encode(encoding)。
- 从外部读取数据时,默认它是字节,然后 decode 成需要的文本;同样的,当需要向外部发送文本时,encode 成字节再发送。
Python2.7中调用print打印出var变量时,操作系统会对var做一定的字符处理:如果var是str类型的变量,则直接将var变量交付给终端进行显示;如果var变量是unicode类型,则操作系统首先将var编码成str类型的对象,再显示。
因此对于unicode中有中文,打印前一定要先encode('utf-8')之类,或者用sys.setdefaultencoding('utf-8')也行。
点击任务栏 Safari
,鼠标放在 清除历史记录...
上,按住 Option
键,该选项变成清除历史记录但保留网站数据
。这样,将不会丢失保存网站的登录信息。
知乎复制文字,经常最后面带版权小尾巴;或者禁止转载的,直接不能复制。
解决方法:Safari任务栏点击开发
→停用JavaScript
,再刷新网页即可。若无开发选项,可在Safari偏好设置
→高级
勾选在菜单栏中显示“开发”菜单
。
感觉 MacOS Sierra 稳定性和发热的控制和 OSX 10.11 相比相差太远,尤其是 WindowServer 的动画过渡上。从 Docker 上打开 Safari,明显感觉到动画的迟滞。可用以下两条指令加快动画:
defaults write NSGlobalDomain NSAutomaticWindowAnimationsEnabled -bool NO
defaults write -g NSAutomaticWindowAnimationsEnabled -bool false
系统整体动画迟滞的原因还有一种可能,就是支付宝服务后台搞事,相关说明和操作详见: 【警告】支付宝后台服务会妨害 rMBP 的显示效能(更新灭活脚本)
升级到 macOS Sierra 后,发现字体册 Fontbook 浏览字体明显卡顿,曾以为是系统特色,后来发现其实重置相关设置就好。在Finder中删除 ~/Library/Preferences/com.apple.FontBook.plist
即可。
遇到 Finder 打开速度极其缓慢,半天才显示文件,也是重置相关设置就好:删除 ~/Library/Preferences/com.apple.finder.plist
后重启Finder。
其实,哪个软件出了问题,删除 ~/Library/Preferences/
下对应的plist文件重置就好。
经 V2EX 讨论,证实为 WiFi TCP Keep Alive 耗电,最简单的设置是终端执行:
sudo pmset -a tcpkeepalive 0
亲测执行此命令后,合盖一晚,掉电为 0。
可能很多人推荐使用Proxychains-ng
,个人觉得还是麻烦。考虑到我使用 zsh 和 surge,因此我的解决方案是: 在 ~/.zshrc
中添加指令:
proxy(){
export https_proxy="http://127.0.0.1:localport"
export http_proxy="http://127.0.0.1:localport"
echo "HTTP Proxy on"
}
noproxy(){
unset http_proxy
unset https_proxy
echo "HTTP Proxy off"
}
终端中键入 proxy
即可打开代理,键入 noproxy
可关闭代理。
[本条来自某锁推的推主] 风扇全速运转时,macOS 会调用 kernel_task 给 CPU 降速,最终导致系统整体迟滞,解决方案是在禁用 SIP 的情况下删除 /System/Library/Extensions/IOPlatformPluginFamily.kext
后重启。详细可参见 Disable OS X kernel_task throttling.
在正在下载的 App 上点击使下载暂停,然后清除 DNS 缓存,再点击继续下载,只要 Apple 服务器和本地网络没问题,速度马上飚满带宽。
OneNote 经常同步出错,或者同步缓慢,根据 MS 官方建议,使用 DNS 4.2.2.1 或 4.2.2.2 即可,然而将其用作备 DNS 简直就是扯淡(备用 DNS 上场的机会都没有)。即使使用了优质梯子还是慢。还好我们有Surge,只需要让 Surge 指定 OneNote 同步的服务器使用 4.2.2.1 或者 4.2.2.2 作为 DNS 就好:在 Surge 配置文件中增加以下几行:
[Host]
d.docs.live.net = server:4.2.2.1
www.onenote.com = server:4.2.2.1
*.microsoft.com = server:4.2.2.1
*.live.com = server:4.2.2.1
再出现同步慢你来打我.
下载 LaTeXiT,键入 LaTeX 生成公式后,直接拖进 OmniGraffle 就好,选择矢量图形,双击还能回到 LaTexiT 编辑。
按住 Cmd
键再点击操作后面的窗口即可。这个操作非常实用,比如复制后面浏览器中的问题到当前的文本编辑器中,这样鼠标选中后面浏览器中的文字复制时,最前的的编辑器窗口并不会消失。
已失效 浏览器使用BaiduExporter或者相关油猴脚本解析导出真实地址再用多线程工具下载。大部分人偏向于使用Aria2下载,但是本人更偏好轻量的Axel,命令行呼出就可用: axel -n num_of_connections -a download_link
亲测20M带宽下,开30+线程,能直接跑满带宽。
终端执行:
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user
然后重启。
升级到 macOS Sierra 后,Mission Control 动画是在太慢,影响效率。可到系统设置的触控板设置中,关闭自带的 Mission Control 手势,然后转用第三方软件 Jitouch 的 Mission Control。
日常编写 AppleScript 完成一些自动化操作时,却不知道某些 App 支持哪些 AppleScript 脚本,其实 macOS 提供了一个很隐蔽的方式来查看:打开脚本编辑器,任务栏点击文件
→打开词典...
,再选择目标 App 即可.
Popclip 是个极好的软件,然而,有些 App 中,由于适配原因,鼠标选中文字后并不能弹出 Popclip 插件条,这时可借助 BetterTouchTool 软件强行让 Popclip 弹出来:
建立一个 .scpt 脚本,里面填入 tell application "PopClip" to appear
保存。然后在 BetterTouchTool 新建一个 Gesture (触摸板手势我选择的是 3 Fingers Swipe Up),Predefined Action
中选择 Open Application / File / Apple Script ...
,在弹出的窗口选中刚才那个 scpt 脚本。这样,每当鼠标选中文字,Popclip 插件条不出来的时候,三个手指在触摸板上顺势网上一推,插件条就强行被手动唤出了.
根据 Google Translate 开发的 Popclip 插件,响应速度是我用过的翻译插件里最快的,可以选择翻译源(.com .cn),有多种国外语言可以翻译,点击按钮后在屏幕右上方显示译文。 ——MAC玩儿法报道
继上次自己开发 Alfred Workflow 后,这次我又盯上了 popclip。
事因是自己一直渴望有一种完美的谷歌翻译使用方式,目的是用鼠标选中待选定句子,然后弹出谷歌翻译内容。无奈一直没人做,只好自己动手实现。
]]>根据 Google Translate 开发的 Popclip 插件,响应速度是我用过的翻译插件里最快的,可以选择翻译源(.com .cn),有多种国外语言可以翻译,点击按钮后在屏幕右上方显示译文。 ——MAC玩儿法报道
继上次自己开发 Alfred Workflow 后,这次我又盯上了 popclip。
事因是自己一直渴望有一种完美的谷歌翻译使用方式,目的是用鼠标选中待选定句子,然后弹出谷歌翻译内容。无奈一直没人做,只好自己动手实现。
展示一下最终的自制 popclip 谷歌翻译插件使用效果:
首先设置里面有几个选项可以设置:
Google Translate Site
: 可选 translate.google.cn
和 translate.google.com
作为翻译服务器,前者给墙内的朋友使用,后者则是给墙外的使用。Destination Language
和 Source Language
: 目标外语 (destination language) 和母语 (source language)。程序将对划中的语言进行检测,若检测的语言非母语,则翻译为母语;若检测的语言为母语,则翻译为目标外语。使用效果GIF:
英译中:
中译英:
确实很方便~
其实 Mac 上的翻译方案很多,在本插件出来之前,个人觉得最好用的当属 Translate Tab了,鼠标选中待翻译字段,点击其 popclip 图标,然后通知栏弹出翻译:
然而 Translate Tab 也是有缺点的:
translate.google.com
,不翻墙无法使用。我给开发者发邮件,开发者回复道 Theoretically google must redirect app to the domain according to geolocation. I will check this moment
,然后我再回复就没有后文了。。。如果用 Surge 的 URL Rewrite 功能设置translate.google.com
跳转到 translate.google.cn
,需要开启 MitM,比较浪费性能。因为我非常喜欢 popclip 这种形式的小软件,因此决定转入 popclip 插件开发。popclip 官方 API 提供了一种 show-result
形式的 GUI 显示文本,然而效果实在太挫了:只能在屏幕中像下面一样显示一长条,不支持换行,太长了就自动截断了,也是让人很醉:
一开始我使用的方案是采用第三方 cocoaDialog 做结果显示 GUI,做了右上角弹出 Bubble 或者屏幕中央弹出 Msgbox 的方式。使用几个版本后还是抛弃了 cocoaDialog,因为弹出的 MsgBox 30 秒后自动消失,往往翻译结果还没看完就消失了。研究 AppleScript 时发现自带的 osascript 提供了 dialog 的组件显示文本,非常好用。就是 dialog 要显示自定义图标文件全网缺少相关文档,找的方案基本不可行,硬是被自己试出来。。。
如何调用谷歌翻译,我第一反应当然是爬虫,然而用 Chrome 到 translate.google.cn 抓包一看:
上图中的 Request URL 中有一个 tk 值,这个值是根据输入的文字调用 JavaScript 计算的,代码经过了非常复杂的混淆包装,几万行代码就为了混淆一个 tk 值,太可怕了。。。
那就试试谷歌官方的收费 API 吧。于是我去开通了 Google Cloud,绑定了信用卡,然后发现新用户居然赠送 300 美金优惠券(限一年用完)!激动!另外,谷歌翻译官方 API 使用方式也很简单,用自己的 API KEY 组合成这样一个 URL: https://translation.googleapis.com/language/translate/v2?key=' + API_KEY
,然后把 {'target':target_lang, 'q': query}
post 上去,就可以得到结果。不需要科学上网,速度也是超级快。然而谷歌计费方式是每 1 M 个 characters 收费 20 美金,我在写代码过程中就随便小测了一下,居然用了 3 K 个 characters,所以这个 characters 咋算的,字母数么。。。要是分享出去用的人多了,用不了几天 300 美金就花光了。
然后我又去搜索,有没有免费的谷歌翻译 API,果然在知乎找到了: 请问如何调用谷歌翻译API?。用 http://translate.google.cn/translate_a/single?client=gtx&amp;amp;sl=en&tl=zh-CN&dt=t&q=query
的形式去查询就可以免费调用谷歌翻译。难道是漏网之鱼?不一会我就发现,woc,同一 IP 下调用此 API 多次就直接被 Google 给 ban 了!心机你 Google!
好在 GitHub 大神多,一通搜索,有人逆向了谷歌的 tk,还封装好了😅: py-googletrans。好了,既然有现成的轮子了,我就直接用了~
虽说第一次接触 popclip 插件的编写,小试一下,发现还是蛮简单的。写起来比 Alfred Workflow 简单不少,然而没法调试,略蛋疼。全程主要参考了少数派 让剪切板在 OS X 上飞起来:PopClip 插件编写教程 和 popclip 官方 github documentation。
一个 popclip 插件,主要包括三部分:
Icon.png: 图标文件。这么好的功能当然要有个卡哇伊的图标啦。
Script: 脚本文件。执行目标功能。popclip 默认只支持 Shell Script 和 Apple Script。要用其他语言,也只能通过这两种 Script 来调用。
其实,最终实现谷歌翻译的 Python 脚本很简单,一共才 60 余行代码,和上次那个优越加速 Alfred Workflow 几百行相比简单太多,逻辑上也更简单。
首先我的 translate.py
通过 argparse
接受以下几个参数:
--site site_arg --srclang srclang_arg --destlang destlang_arg
然后通过 go.sh
来执行此 py 文件:
/usr/bin/python translate.py --site $POPCLIP_OPTION_SITE --destlang $POPCLIP_OPTION_DESTLANG --srclang $POPCLIP_OPTION_SRCLANG $POPCLIP_URLENCODED_TEXT &
上面的 $POPCLIP_XXX
是 popclip 传递来的变量。以 --site
为例,在上面提到的 Config.plist文件的 Options 中,我设置了一个标题为 Google Translate Site
的多选栏,设定一个变量,其 key
设定为 site
,value
设定为 translate.google.cn
或 translate.google.com
(都是字符串),靠用户来选择使用哪个 value。然后选定的 value 以名为 $POPCLIP_OPTION_SITE
的变量存在于整个插件运行的过程中,可随时调用。同样的,$POPCLIP_OPTION_LANG
代表 Target Foreign Language
那一栏的变量的 value,这里的详细设置参见我的 Config.plist。
然后有必要交代一下最后那个 $POPCLIP_URLENCODED_TEXT
。如何去把鼠标选中的文字转换成一个变量,传递给脚本呢。
起初,我是在 Config.plist 中的 Actions 中是这么设定的:
<key>Before</key>
<string>copy</string>
然后用的变量名为 $POPCLIP_TEXT
。Before 指的是在 popclip 执行主要 action 之前要干的事,这里我设定为 copy
。这样,只要鼠标选中一段文字,将自动执行 Command + C
,同时以 string 的形式保存为一个名叫 $POPCLIP_TEXT
的变量。这样的弊端也是显而易见的,每次鼠标划词都复制文字到剪切板,会扰乱剪切板。因此我改用:
<key>Requirements</key>
<string>copy</string>
根据官方说明,这种情况下的 copy
就不会复制文字到剪切板了。另外之所以用 $POPCLIP_URLENCODED_TEXT
,是因为坑爹的 $POPCLIP_TEXT
,在遇到空格时就会停止复制,比如鼠标选中了 a b c
,其实变量只存了 a
。所以只好用 url-encoded 的形式,最后再用 urllib 库的 unquote 转换为正常的 String。
最后,整体流程框图如下:
整体下来思路还是挺清晰的。然后只需要把以上所有文件放在一个文件夹中,强行加上一个 .popclipext
就大功告成啦~
虽然代码很简单,开源到 GitHub 了,方便一下其他人参考编写 popclip 吧~
]]>信息量可以理解为不确定性的多少。
香农信息量 (以 2 为底)
上式中,p 越小,则不确定性越大,包含的信息量就越多。比如 32 支球队,在无任何先验信息的前提下,用二分法猜冠军队伍,最多猜 5 次,也就是 。
香农信息量的单位是比特 (bit)。
]]>信息量可以理解为不确定性的多少。
香农信息量 (以 2 为底)
上式中,p 越小,则不确定性越大,包含的信息量就越多。比如 32 支球队,在无任何先验信息的前提下,用二分法猜冠军队伍,最多猜 5 次,也就是 。
香农信息量的单位是比特 (bit)。
信息熵:香农信息量的期望
信息熵的一种解释是,它表示的是最短的平均编码长度。
同样的,不确定性越大,熵就越大。
又叫 KL 散度(Kullback-Leibler Divergence)
它用来衡量两个数值为正数的函数的相似性。
很容易证明,有三个结论:
(1) 两函数完全相同时,KL=0
(2) KL越大,差异越大
(3) 对概率分布或者概率密度函数 (>0), KL 可用来衡量两个随机变量分布的差异性
注意,KL 散度是不对称的,因此琴生和香农提出以下计算方法:
对一随机事件,其真实概率分布为 ,从数据中得到的概率分布为 ,则我们定义,交叉熵为:
理解:
我们用 p 来衡量识别一个样本的信息量,也就是最小编码长度:
我们用 q 来估计真实分布为p的样本的信息量:
则估算多出来的冗余信息量(正好就是KL散度啊)
在机器学习中,p 通常设定为真实标记的分布,q 设定为训练后模型预测标记的分布。
很容易发现:
即:交叉熵=信息熵+KL散度(相对熵)
由于信息熵 是固定不变的,因此我们在机器学习中就用交叉熵作为损失函数。常见的做法是先用 Softmax 函数将神经网络的结果转换为概率分布,然后用交叉熵刻画估算的概率分布与真实的概率分布的“距离”。
显然,
可由吉布斯不等式证明得到:
x>0时,有
因此,
等号当且仅当在 p 和 q 分布完全一致时成立 ()
参考资料:
]]>硬件平台:i7-6700K CPU, GTX1080 GPU, 华硕 Z170 主板
软件平台: Ubuntu 16.04
先看看网上最流行的3个不靠谱解决方案:
基于我自己和我身边朋友的经历,以上三个方法根本无法解决开机无限登录的问题。
其实当我走完了所有流程后发现,其实出现安装失败,无限重启的根由是:安装独显驱动的时候不要让独显处于被占用状态,否则出错。什么叫被占用状态呢?当你把显示器的线( 通过HDMI 或者 DP口)接在独显上,一旦开机图形系统被自动后台启用后,独显就被占用了。
那么解决的方案其实很简单:
以下是稍详细的方案说明:
这里我安装的是 Ubuntu 16.04 系统。若在安装过程中遇到开机黑屏,或者显示器提示频率超出范围,在开机时修改 grub 设置即可:开机时狂按键盘上的e
键,进入 grub 编辑界面,在 quiet splash 后面加上 nomodeset。添加该参数的目的是让内核在加载 X11 Window 系统前不要加载视频驱动程序,转而采用基本的 BIOS 模式,避免前面的黑屏等不兼容情况出现。
Nouveau 是 Linux 诸多发行版为 Nvidia 显卡提供的默认开源驱动程序,在显示效能上当然比不上 Nvidia 官方提供的默认驱动。因此,首先我们要卸载这个开源驱动,为安装官方闭源驱动做准备。
打开终端执行 (下面以 gedit 文本编辑器为例):
sudo gedit /etc/modprobe.d/blacklist-nouveau.conf
在新增的文件中写入以下内容保存:
blacklist nouveau
blacklist lbm-nouveau
options nouveau modeset=0
alias nouveau off
alias lbm-nouveau off
然后在终端中执行:
echo options nouveau modeset=0 | sudo tee -a /etc/modprobe.d/nouveau-kms.conf
sudo update-initramfs -u
检查 nouveau 开源驱动是否屏蔽成功:
lsmod | grep nouveau
若无内容输出,则说明 nouveau 已经屏蔽成功,可以进行下一步操作了,否则仔细检查以上操作有无操作上的失误。
安装驱动的方式有好几种,比如系统设置附加驱动安装法,官方 .run 驱动文件安装法,ppa 法和自行编译法。这里将前三种都大概讲一下,根据情况任选其一即可。
注意:这种方法仅适用于拔线法,不适用于没有集显的CPU使用。因为这种方法是在图形界面里执行的,而没有集显口可用的情况下,独显是一直被占用的。
在系统设置里面更新软件源缓存列表后,在附加驱动里面勾选类似于 Nvidia-387 字样的私有驱动进行安装,装完重启电脑即可。
这一步若遇到安装 N 卡驱动时进度条一直卡在最后走不满的情况,可进终端用 top 指令查看是什么进程在占用 CPU。若是 aptd 进程长时间占用 100% CPU,在系统设置里面关闭所有系统更新相关的设置即可解决。
首先大致介绍一下 Ubuntu 的桌面图形系统的运作方式和相关概念。第一个概念是 DE (desktop environment), 即桌面环境,可以理解为整个图形界面的 GUI 显示模式,不同的 Ubuntu 定制版和同一定制版的不同版本采用的 DE 可能都不一样,常见的 DE 有界面简洁但是资源占用少的 Xfce,Ubuntu 14 和16 都采用的 Unity,以及 Ubuntu 17 后开始采用的 gnome 等等。第二个概念是 DM (Display Manager), 前面提到了 DE,用于管理 DE 的软件就是 DM 了,比如可以同时安装多个 DE,用 DM 去手动切换。Ubuntu 16 采用的 DM 叫 lightdm。
因此,这里安装显卡驱动时,我们直接把整个 DM 关闭掉就不会让 DE 占用独显了。DM 的存放位置在 /etc/init.d/
文件夹下,里面一般带 dm 后缀的就是系统默认 DM 了。
ctrl
+alt
+F1
进入文本模式:
关闭图形界面:
sudo /etc/init.d/lightdm stop
添加 ppa 源并更新缓存:
sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt-get update
安装驱动:
sudo apt-get install nvidia-xxx # xxx是版本号,这里按tab选择版本即可
重启电脑或重新打开图形界面(sudo /etc/init.d/lightdm start, 然后 ctrl alt F6进入图形界面)
Nvidia 提供的驱动文件格式是 .run 后缀的,去官网下载对应的驱动文件,然后按照 ppa 法关闭图形环境,在文本模式输入:
chmod a+x a.run # a.run为驱动文件
sudo sh ./a.run
装完重启或者重新打开图形界面。
如果操作过程中发现安装出错了要重新装驱动,首先得卸载驱动残余,再重新安装驱动:
sudo apt-get remove --purge nvidia*
sudo apt-get autoremove
sudo reboot
注: .run文件安装的驱动最好上面卸载可能不干净,最好先执行: sudo /usr/bin/nvidia-uninstall
上述几种方法安装完驱动后,开机进入系统,屏幕右上角点击关于此计算机,图形一栏应该会显示这是NVIDIA 的显卡,说明独显驱动已安装成功。若仍然显示为 Intel 集显,可尝试手动切换到独显:
sudo prime-select nvidia
这时再输入指令:
prime-select query
若显示 nvidia
说明已切换到独显,但是要重启才能生效。
如果这一步仍然失败,一定是前面哪一步出错了。
在拔线法的情况下,安装 N 卡独显驱动前,需要把显示器线接在集显口上。然而某些华硕主板设置是默认关闭集显的,因此要先在主板设置中将其打开并暂时将优先级设为最高,再插线到集显接口,否则该接口无法使用。
更改方法:
开机时进入BIOS设置,依次进入 Advanced—>System Agent Configuration—>Graphics Configuration中,将 iGPU 设为开启,然后 Primary Display 设为 IGFX,保存重启。再把视频线接到主板上 CPU 那儿的集显接口上,开机进入 Ubuntu 系统。
根据第 3 部分教程安装完独显驱动后,再把显示器线接到独显口上,这时再去主板中启用独显:
上一步的步骤中把 Primary Display 设置为 PCIE 或者 Auto
总结起来一句话:给独显装驱动的时候,不要占用独显。凡是线插在独显口,又进入 GUI 图形界面操作的,一定失败!
]]>