作者归档:dontpanic

Codeforces 1A Theatre Square

长和宽除以边长后向上取个整。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace _1A
{
    class Program
    {
        static Int64 calc(Int64 i, Int64 a)
        {
            return i / a + (i % a == 0 ? 0 : 1);
        }

        static void Main(string[] args)
        {
            Int64[] ints = Console.ReadLine().Split().Select(i => Int64.Parse(i)).ToArray();
            Int64 w = calc(ints[0], ints[2]);
            Int64 h = calc(ints[1], ints[2]);

            Console.WriteLine(w * h);
        }
    }
}

写出形似QML的C++代码

最开始想出的标题是《Declarative C++ GUI库》,但太标题党了。只写了两行代码,连Demo都算不上,怎么能叫库呢……后来想换掉“库”这个字,但始终找不到合适词来替换。最后还是起了个low一点的名字,贱名好养活啊!

这篇文章的目的是介绍如何用C++写出带有Declarative风格的代码。有一些GUI库需要额外的预处理过程(比如qt),还有一些也支持XML格式的GUI声明,但需要运行时Parse那个XML(比如wxWidgets)。能不能只用一个C++编译器、不要运行时Parse新语言来搞定这个问题?

直观地看上去,QML语法跟C++好像还有几分像,就选择QML进行借(chao)鉴(xi)吧。
最终的代码放在了 https://github.com/dontpanic92/yz ,代码与文章一同食用味道更佳。

继续阅读

编辑器背后的数据结构

大约刚上大二的时候,想做一个编辑器控件。不是一个用Scintilla套上外壳的编辑器,而是一个能被套上外壳的控件。当然它最后也成为了我众多流产了的练手项目中的一员,不过人人黑历史里还留存着当时的一张截图

那段时间也对编辑器所使用的数据结构非常感兴趣。我们需要一种数据结构,能够支持字符串高效地索引、遍历、插入和删除。当时找的一些论文和书还躺在硬盘里一直没删,如今拿出来再嚼一嚼。下面介绍几种在编辑器中常见的数据结构。

继续阅读

贝叶斯分类器

这是3月25日我在TM组机器学习讨论会上的分享。

Content


  1. 贝叶斯决策论
  2. 朴素贝叶斯分类器
  3. 半朴素贝叶斯分类器
  4. 贝叶斯网络

1. 贝叶斯决策论


贝叶斯决策论是一种基于概率的决策理论。当所有相关的概率都已知的理想情况下,贝叶斯决策论考虑如何基于这些概率和误判损失来选择最优的类别标记。

Example

哈工大与哈师大的同学举办大型联♂谊♀会,两个学校分别有500人参与。在联谊会上随机找到一个同学,请猜测他是那个学校的学生?

如果我们一点额外信息都不知道的话,只能随机猜测给出答案。如果我们能够提前知道一点点信息的话,就能够更大程度地猜中正确答案。比如,性别信息:

 

如此的话,假若这个同学是男生,我们肯定会猜测他是哈工大的学生。而从贝叶斯决策论的角度来看,我们需要比较以下两个概率大小:

  • P(工大学生=是 | 性别 = X)
  • P(师大学生=是 | 性别 = X)

上述两个概率被称作后验概率。后验概率往往难以直接获得,我们需要采用一定的手段进行计算。一些算法采用直接对后验概率进行建模的方法,例如SVM、决策树等,这些模型称为判别式模型。而先对联合概率进行建模、进而计算后验概率的模型,称为生成式模型

\(P(c|\boldsymbol{x})=\frac{P(\boldsymbol{x}, c)}{P(\boldsymbol{x})}=\frac{P(c)P(\boldsymbol{x}|c)}{P(\boldsymbol{x})}\)

由此可以计算得到,P(工大学生=是 | 性别 = 男)为4/5,P(师大学生=是 | 性别 = 男)为1/5.

在上面的例子中,我们直接使用了后验概率对类别进行估计。实际问题中,如果将某一类估计错误的代价比较大的话,可以选择在后验概率前乘以一个系数,变为期望损失。分类也从最小化分类错误率变为最小化期望损失。

在上面的式子中,\(P(c)\)代表的是类先验概率。在样本足够大的情况下,直接使用频率即可作为这一概率;\(P(\boldsymbol{x}|c)\)叫做类条件概率,它跟属性x的联合概率有关。上面的例子中,x只有一维,而在实际问题中,往往会选择很多个Feature。此时他们的联合概率就变得难以计算,因此我们需要一些手段对它们进行估计。

继续阅读

Goodbye WordPress

花了点时间把博客从Wordpress换到了Ghost上。一是觉得Wordpress有点慢,TTFB(Time To First Byte)常年在1s左右,状态不好能到几秒十几秒。换到Ghost之后,基本稳定在400毫秒。要是把服务器换回旧金山应该还会更好一点。二是我基本上就用Markdown随便写一写,有高亮有公式就够了,Ghost后台也足够简洁,自己把Prism.js代码高亮和MathJax公式加上就好。

不过迁移的时候,导出插件把我的代码全都弄乱了……还需要人工调教,甚至每段代码中<<之后的代码全部丢失!!如果你也打算从Wordpress导出Ghost格式的数据,一定要记得备份代码!

再就是基本找不到称心的主题,还是先用默认的Casper吧,加了个封面之后感觉倒是好了很多。

站在镜像源背后的男人

@雨翌 说想知道镜像源是如何工作的,我们就来说一说这个。其实要做一个镜像源,需要搞定的有两件事:一是如何向广大用户提供下载服务,一条httpd start就搞定了;二是如何与上游同步,然而这种东西也一条rsync就搞定了:joy:

关于rsync

rsync提供了一种快速的文件同步的机制。一般的diff算法需要分别遍历两个文件,然而这种功能并不适用于远程传输:如果我能够同时获得需要同步的两个文件,再diff他们就没什么意义了。rsync使用的算法并不是特别复杂,可以Google The rsync algorithm 搜到Andrew Tridgell在1996年的论文,man rsync可以得到使用说明。

继续阅读

Brainfuck JIT Compiler in Rust

Hello JIT


JIT不是一个神秘的玩意。

—— Tondbal ik Ni

我们都知道,对于解释型的语言实现来说,性能是大家关注的焦点。比如,这位 Tondbal ik Ni 曾经还说过:

P*没上JIT,慢的一逼!

—— Tondbal ik Ni

似乎这句话总是隐含着另一层意思:实现JIT,难!而当我们再一联想到JVM这种庞然大东西的时候,很自然的就

然而!JIT原理并不复杂,做出一个玩具JIT Compiler更是非常轻松。之所以JVMs那么庞大而复杂,原因之一在于它们做了大大大量的优化工作。

我们今天就要来看看JIT究竟是个什么东西!

Just-in-Time Compiler


JIT Compiler,究其根本还是一个Compiler。一个Ahead-of-Time Compiler的编译过程往往会有这些(既不充分也不必要的)步骤:

  • 词法分析
  • 语法分析
  • 语法制导定义或翻译
  • 中间代码生成
  • 代码优化
  • 目标代码生成

对于解释器来说,往往将编译工作进行到中间某一步后就直接进行解释执行了,并不生成目标代码。而JIT Compiler却是要生成目标代码的,最终执行的是编译好后的Native Code。只不过,它将目标代码生成的部分推迟到了执行期才进行。这样的好处有:

  • 无需重新编译就可以实现跨平台
    参考Java,它将平台差异隔离在了中间代码部分(指Java ByteCode)。
  • 运行时优化
    当年大欢还在用Gentoo的时候曾经开过嘲讽:本地编译,优化开的细,比你Arch强多了(然而后来还是倒戈到了Arch 666666)而一个JIT Compiler不仅知道目标平台的信息,更知道程序的运行时信息,因此往往可以有更多的优化。
  • 解释/编译混合
    这其实也可以看作是一种优化措施,即执行次数多的代码JIT编译后执行,执行次数少的代码解释执行。因为JIT还是需要一步编译的过程,如果代码执行次数少,很可能抵消不了编译过程带来的时间开销。

所以,其实优化是JIT Compiler中相当重要的一部分。如果我们不要优化,那可是简单了很多哟。

Core of JIT


如果你能看懂这段代码,那就说明你已经掌握了JIT的精髓了:

#include <stdio.h> 
#include <string.h>
#include <sys/mman.h> 

char f[] = {0x48, 0xC7, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC3}; 

int main(){  
    int a = 5; memcpy(&f[3], &a, 4); 
    char* mem = mmap(NULL, sizeof(f), PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0); 
    memcpy(mem, f, sizeof(f)); 
    printf("%dn", ((int (*)())mem)()); munmap(mem, sizeof(f)); 
    return 0;
 }

其中mmap的作用是申请按照PageSize对齐的内存,并可以修改页面保护属性。

所以一个JIT Compiler编译的主要步骤就是:

  • 申请一块可写、并且将来可以改成可执行的内存。
  • 生成机器代码,写入内存。
  • 修改内存页面属性,Call it!

So easy,下面进入脑残环节。

An interpreter for Brainf*ck


我们将实现一个Brainfuck的解释器,随后再实现一个JIT编译器。之所以选择Brainfuck,自然是因为它相当简单,完全可以当做中间代码进行处理,省去了词法语法分析、中间代码生成等与编译原理直接相关的部分。

解释器写起来就太简单了。Brainfuck预设我们有一块无限大的内存,初值均为0。数据指针指向第一个字节。它支持8种操作:

  • >
    数据指针右移一格
  • <
    数据指针左移一格
  • +
    数据指针指向的内存内容+1
  • -
    数据指针指向的内存内容-1
  • .
    putchar
  • ,
    getchar
  • [
    如果当前内存内容 == 0,则向后跳转至对应的]
  • ]
    如果当前内存内容 != 0,则向前跳转至对应的[

翻译器部分可以作为大一的C语言实验哈哈哈哈

A JIT Compiler for Brainf*ck


如果要手撸JIT Compiler,则需要对目标平台有一定的了解。我们这里的目标平台是x86_64,这个网址 可以在线将汇编生成为x86或x64的机器代码。

第一步:申请PageSize对齐的内存
这一步除了使用mmap,还可以使用posix_memalign。

第二步:生成函数框架
在这里我们将一整个Brainfuck程序编译成一个函数。这个函数接受一个参数,即我们事先申请好的一块内存作为数据区域。对于x64来说,Linux等类Unix系统遵循的调用约定是System V AMD64 ABI,函数的第一个参数由Rdi传递。因此我们生成的函数的开始与结束部分如下:

pub fn compile_and_run(&mut self, code: &str) {  
    self.jit_code.emit_push_r(Reg::Rbp); 
    self.jit_code.emit_mov_rr(Reg::Rbp, Reg::Rsp); 
    self.emit_push_regs(); //Save regs 
    self.jit_code.emit_mov_ri(Reg::Rbx, 0); //Rbx = data_pointer 
    self.jit_code.emit_mov_rr(Reg::Rdx, Reg::Rdi); //Rdx = memory_base 

    //more code here... 

    self.emit_pop_regs(); //Load regs that saved before 
    self.jit_code.emit_mov_rr(Reg::Rsp, Reg::Rbp);
    self.jit_code.emit_pop_r(Reg::Rbp);     self.jit_code.emit_ret(); 
}

上面的代码中,各个emit函数的作用是生成相应的机器代码,放入内存中。例如emit_push_r(Reg::Rbp)将生成机器码0x55,它对应的汇编为push rbp

接下来就是根据Brainfuck各个操作生成机器码了。例如>操作:

self.jit_code.emit_inc_r(Reg::Rbx);  

So easy. 需要额外说明的只有两点:

一是我们可能需要重定位操作。当我们的buffer空间不够的时候,需要对其进行扩大,这样的话我们代码所在的 地址就会变动。而有一些指令(比如Relative跳转、RelativeCall等)它的操作数是当前RIP(即程序计数器PC)与目标地址的 Offset,这就需要当我们最终结束生成这个函数时,再对这些指令的操作数进行计算。

二是对于[操作来说,需要一个patch back的过程。当我们在编译过程中遇到它的时候,我们并不知道跳转的目的地址是哪里。只有在遇到对应的]时,才能更新它的跳转地址。

第三步:Call it!

let mut memory: Vec<u8>= vec![0; 10000];  
let func: extern "C" fn(*mut u8) = unsafe { mem::transmute(self.jit_code.function()) };  
func(memory.as_mut_ptr());  

Performance Comparison


我找了一个用Brainfuck写的Mendelbrot程序进行测试,它会在控制台输出Mendelbrot的ASCII图(大神请受我一拜 )。除了上面自己实现的解释器和JIT编译器外,我还找了一个Brainfuck的编译器bfc进行测试。

测试结果大致如下:

Interpreter: ~1min
JIT: ~4.31s
bfc: ~4.21s

代码请参 https://github.com/dontpanic92/rainfuck

最后,由于将汇编翻译为机器码是一件体力活,我们还可以使用一些现成的工具。例如DynASM,通过预处理的方式将C + ASM混合代码处理为C语言代码(即省去了我们显示emit的部分)。或者,也可以考虑使用LibJIT或LibGCCJIT等库。

Reference


  1. http://blog.reverberate.org/2012/12/hello-jit-world-joy-of-simple-jits.html
  2. http://www.hydrocodedesign.com/2014/01/17/jit-just-in-time-compiler-rust/
  3. http://www.jonathanturner.org/2015/12/building-a-simple-jit-in-rust.html

使用Reflexil修改Unity3D游戏逻辑

曾经有个游戏叫做《新剑侠传奇》,曾经停留在一个尴尬的版本1.0.7。作为一名抠脚大汉,自然要坐在在电脑前为广大玩家发福利!

故事背景

游戏发售之后出现了一些Bug,官方在更新至1.0.7版本后决定回炉。然而这一版本依然有一些玩家比较需要的Feature没有实现。在长达两周多的回炉阶段,我的补丁横空出现啦!

技术背景

MSIL可以较为轻易地反编译回C#/VB代码,如果不做代码混淆的话,可读性非常高。目前很多国产单机游戏都选择Unity 3D做为引擎:scream: ,并使用C#作为主要逻辑的实现语言。这为我们修改游戏逻辑创造了相当好的条件。

实现功能

  • 打击感增强
    通过在命中目标后停顿画面0.1秒实现。
  • 新增操作模式
    《新》是一个具备跟随视角的3D游戏,鼠标旋转视角,鼠标点击怪物为普攻,感觉略坑爹。新增操作模式采用类似龙之谷的方式:默认隐藏鼠标,鼠标移动旋转视角,鼠标点击为普攻,ctrl键呼出鼠标。
  • 摄像机位置记忆
    每次剧情之后摄像机都会回到初始位置,不会记忆上次用户调整的摄像机与人物的距离。
  • 暂停游戏
    进入战斗或者开始剧情之后,终于可以随意去上厕所了……
  • 跳过开始动画
    打开游戏时无需强制观看片头。
  • 画面改善:对比度增强、Bloom、HDR
    使用Unity内置Shader实现。

工具准备

  1. .NET Reflector
    我们用 .NET Reflector 来对 .NET Managed DLL 进行反编译工作。对于没有进行混淆或加密操作的程序,可以得到可读性很高的代码。
    0<em>1448775094110</em>1.png
  2. Reflexil
    Reflexil 可以在已经编译好的Managed DLL中插入MSIL代码。它提供了.NET Relfector的插件,安装好后即可直接插入IL代码。
  3. XamarinStudio (MonoStudio)
    其实这个有无皆可。但是如果不想所有代码全部使用IL手写的话,还是搞一个比较好。

代码修改

拿打击感增强作为一个例子,我们来看一下如何修改。Unity 3D游戏的主体逻辑都在/xxxx_Data/Managed/Assembly-CSharp.dll中。这个游戏的逻辑由几百个类/枚举构成,如果游戏大量使用了Unity插件(比如可视化编程:see<em>no</em>evil: ),可能还会更多。

增强打击感的方法是在玩家命中敌人后画面停顿0.1秒。所以我们需要在命中时记录当前时间、停顿画面,然后在0.1秒后取消停顿。

经过浏览代码,发现负责进行攻击的逻辑集中在Fighter类中。我们需要修改的有两个函数:一个是CalcHurt,发生在玩家已经命中怪物之 后,用于计算伤害;另一个是Update,每次迭代更新逻辑时都会调用这个函数。我们还需要一个Field用于记录停顿时间,可以直接在类上右击通过 Reflexil添加。

打开CalcHurt方法,在Reflexil中添加IL代码。IL代码非常简单易懂,而且我们并不需要掌握所有的IL指令。简介可以参考Introduction to IL Assembly Language,或者在Common Language Infrastructure (CLI) | ECMA-335中有所有指令的列表。

在修改完IL代码后,.NET Reflector会重新加载并反编译代码。如果指令有错误,.NET Reflector会无法反汇编成功。

停顿功能使用了Unity提供的Time.settimeScale函数,获取时间使用Time.getrealtimeSinceStartup。这些函数可以参考Unity 3D Manual

举例来说,

Time.set_timeScale(0.001f); InjectedField = Time.get_realtimeSinceStartup + 0.1;  

的IL代码如下:

ldc.r4 0.0001 call Time.set_timeScale call Time.get_realtimeSinceStartup ldc.rc 0.1 add stsfld InjectedField #InjectedFiled为静态成员  

就是这么简单啦。

使用XamarinStudio

如果不想所有代码全部手写,可以使用XamarinStudio新写一个Class,将所有新的逻辑放在这里。然后再修改原来的Assembly-CSharp.dll时,所有位置都变成函数调用就行了。

在编写新Class时,需要把游戏的Managed目录中的相关DLL作为引用添加进我们的工程里。对于这个游戏来说,我们主要用到了 UnityEngine、Assembly-CSharp、Assembly-CSharp-firstpass、Assembly- UnityScript-firstpass。

写好Class后,可以使用Reflexil将改Class添加进Assemby-CSharp的引用中。
0<em>1448778641887</em>3.png

请原谅时间久远电脑上已经没有XamarinStudio了:joy:

替换原始文件

没什么问题之后,就可以去论坛或者贴吧收获一番经验了~

关于Intel超线程技术

原回答自知乎:奔腾四和SNB之后的超线程技术一样吗?

1.Pentium 4 HT(NetBurst)和Core i7(Nehalem)中的超线程技术一样吗?

Intel的超线程技术在2002年2月发布的志强处理器中首次出现,并在同年12月发布的Pentium 4 HT中同样加入了这一技术[1]。关于此时的Intel的超线程技术的论文比较多,比如(Deborah T. Marr et al, 2002)[2]。论文中提到,architecture state在各个逻辑线程中是独立存在的,而其余几乎所有资源都是共享的。Architecture state主要包括的是各种寄存器,例如通用寄存器、控制寄存器CR以及APIC寄存器等;而共享的包括缓存、执行部件、分支预测、控制逻辑以及总线。

而后来在Nehalem核心(Core i7)中重新回归的超线程技术,我没有找到官方详细的论文。不过找到了一个德克萨斯A&M大学博士生的论文,关于Nahalem核心的[3]。文中同样提到:对于同一个物理核上的逻辑核心,它们之间

  • 独立:寄存器状态、返回栈缓冲(用于预测函数返回指令)、指令TLB(用于加速虚拟地址与物理地址的转换)
  • 分隔(静态划分):各种缓冲
  • 竞争共享:分为SMT-sensitive 和SMT-insensitive。保留栈(用于实现乱序执行)、数据TLB、二级TLB等属于SMT-sensitive,所有执行单元均为SMT-insensitive。

以及共享Cache。

所以结构上看,基本一致。

两篇论文中都有提到CPU的Pipeline(流水线结构)。当Nehalem开启超线程时,将从两个线程中取指令,之后送入后端乱序执行引擎执 行。而[1]中提到得Pentium 4在开启超线程后的做法则更为详细:两套指针分别记录两个线程的PC,解码后存入Execution Trace Cache(其为一级指令缓存的主要部分)。两个线程再按照时钟周期依次访问这个Cache。如果有一个线程stall住了,则另一个随意用。

大致来看,主要步骤基本相同。不过细节不太清楚。

2.什么是超线程?

超线程(即Simultaneous multithreading[4])属于线程级并行方法。在说超线程技术(多线程技术)之前,我们先来看一下超标量技术。

超标量技术是一种指令级并行技术,具备超标量特性的CPU具有同时发射多条指令的能力,这样CPU就可以充分利用各种执行部件。举例来说,如果单发 射一条加法指令,那么在执行过程中只有加法部件(或算数计算部件)被利用起来;如果我有两条指令:add R1, R2; shr R3, 3(加法和右移),如果两条指令同时执行,那么加法部件和移位部件就同时利用起来了。

然而能够同时执行的前提是:两条指令之间不能有相关性。例如第一条指令需要写R1,第二条指令需要读R1,那么这两条指令无法同时执行。由此即诞生出了乱序执行(Out of Order)技术:如果第三条指令与前两条指令没有相关性,则可以先执行第三条指令。

超线程技术则可以更加充分地利用执行部件:同时执行两条线程,进一步降低了指令之间的相关性。同时,如果一个线程需要访存(意味着大量的时钟周期可能被浪费)、或是分支预测失败(意味着需要清空流水线),超线程的优势就更明显了。

然而对于超线程技术的优劣则有激烈的讨论,主要集中在性能的提升的同时芯片面积与功耗等也在增加,这样的trade-off是否合适。

3.关于超线程技术消失又重现

Pentium Pro、Pentium 2、Pentium 3所使用的架构均为P6架构,Pentium 4使用Netburst架构,后续的Core 2使用的是Core架构,从Core i系列开始进入Nehalem/Sandy Bridge架构等[5]。

然而,NetBurst是相对独立的一个架构,让人记忆深刻的就是超长的流水线和相当高的频率。而Core架构是基于P6的变种Pentium M的架构改进而来的。在Intel的一个Slides中也可以得知,NetBurst和P6的Pipeline差异较大,Nehalem的基础需要在二者 中做出选择。而最后做出让多线程回归的决定,也是一种“抉择”[6]。

Reference

  1. Hyper-threading
  2. Marr D, Binns F, Hill D L, et al. Hyper-Threading Technology Architecture and Microarchitecture[J]. Intel Technology Journal, 2002, 6(1):1.
    http://www.cs.sfu.ca/~fedorova/Teaching/CMPT886/Spring2007/papers/hyper-threading.pdf
  3. Thomadakis M E. The Architecture of the Nehalem Processor and Nehalem-EP SMP Platforms[J]. Jfe Technical Report, 2011.
    http://sc.tamu.edu/systems/eos/nehalem.pdf
  4. Simultaneous multithreading
  5. Nehalem (microarchitecture) P6 (microarchitecture) Intel Core (microarchitecture)
  6. http://web.stanford.edu/class/ee380/Abstracts/100217-slides.pdf

深度学习简介及单词的向量化表示

Content

  • 深度学习简介
  • NLP与深度学习
  • 单词的向量化表示

1. 深度学习简介

首先应当明确的是,深度学习是机器学习中的一个领域。然而与传统机器学习所不同的是,传统的机器学习的重点在于特征的设计。在设计过特征之后,就变成了研究如何调整权重、优化参数来得到一个最优的结果。

图片1

然而特征设计所涉及的知识、经验的储备往往只有博士级别的研究人员才能够得心应手,而且特征设计的优劣往往直接影响最终的分类结果。与之相反,深度学习应用的是多层特征学习,其中特征学习指的是计算机能够自动地学习到特征的表示,这就解决了手工选择特征局限性较大的问题。深度学习提供了一个近乎统一的框架。它够表达各种信息,能够自动学习,并且非常灵活。这个框架也同样支持监督学习与非监督学习两种学习方式。

除此之外,深度学习还具备以下特点:

  1. 数据量增大时,深度学习的获益更多;
  2. 能够利用多核CPU、GPU加速
  3. 不断涌出的新模型、新算法
  4. 最终性能的提升

2. NLP与深度学习

事实上,在NLP领域,深度学习已经开始展现它的威力了。在应用方面,机器翻译、情感分析、问答系统等都依靠深度学习取得了重大的进步;而在NLP的各个层次上,例如语音识别、形态分析、句法分析、语义解释等,深度学习也发挥了重要的作用。接下来

选区_006

语音

在语音层面,传统的表示形式采用了表格的形式来表示每一个音素。而在深度学习中,将每一个音素表示为向量。

字形

对于一个单词来说,传统的表示形式将词根、前缀和后缀分开表示,例如un-interest-ed。而在深度学习中,往往将一个词素表示为一个向量。

图片3

句法

传统的表示方法会显示地标注出每一个短语的具体类别,例如VP、NP等。而在深度学习中,会将一个词或短语均表示为向量。

语义

lambda演算是一种在语义层面的传统表示方式。例如,将likes视作接受两个参数的函数,每一次合并都相当于柯里化的过程:

图片4

而在深度学习中,仍然将语义表示为向量的合并。

可以看出,向量形式是NLP中所有层次的表达形式。


3. 文本的向量化表示

意义

语言文字或一些符号具备一定的意义。例如,苹果和apple所指代的意义相同。而对于一个外星人来讲,■△▲〓※↓↑〓↓说不定就代表“苹果”。因此,如果想要让计算机理解这一层意义,那么它也需要一种表示;反之,如果我们认为某种表示是计算机所理解的“苹果”,那么即便我们无法阅读(例如■△▲〓※↓↑〓↓),这也是在合理范围之中的。

意义在计算机中的表示

一种表达意义的方式是利用同义词和上位词。例如,熊猫属于动物一类,我们可以用“动物”来表达熊猫的某些方面的意义。再比如,good和well有时意义相近,那么我们可以用well来代表good的某些方面的意义。

然而,这样的表示的问题在于:

  • 缺乏语义差别的信息
  • 难以适应新词
  • 主观
  • 需要人工标注
  • 相似度难以计算

究其根本原因,在于这种方式将单词视为了一种原子符号。或者说,如果在向量空间中对其进行表示的话,我们把它叫做one-hot:

选区_007

one-hot的问题在于,无法表示单词之间的意义相近的关系。例如,hotel和motel两个词,如果利用one-hot形式来表示的话:

hotel  = [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]

motel = [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]

我们无法使用向量运算来获得它们之间意义上的相似度。

You shall know a word by the company it keeps.

–J.R.Firth

J.R.Firth是50年代著名的语言学家,他的这句话被誉为当代NLP领域中最成功的思想之一。对于一个单词的意义,较好的方式是考虑它的上下文。

图片5

而这个“上下文”,则有不同的范围可以选择:其一是将整个文档作为上下文,另一种是选择一个较小的固定的窗口作为上下文。对于“全文档共现”来说,单词向量的维数是文档的个数,如果这一单词在某一篇文档中出现,则该维置1。全文档共现的方式能够挖掘单词的隐含语义。例如,对于“足球”和“教练”来说,它们很有可能在多篇文档中共同出现,则它们的向量也有多维同为1;如果取and的话,就可以获得它们之间的相似程度。

而对于基于窗口的共现来说,其能够同时获得语法与语义的信息。下面我们通过一个例子来看一下基于窗口共现方式的词向量学习过程。

我们选取三句话:

  • I like deep learning.
  • I like NLP.
  • I enjoy flying.

窗口大小取为1,即向前看1一个词、向后看1个词,窗口中一共3个词。通过对共现次数的计数,可以得到以下的矩阵:

图片6

我们可以发现,这个矩阵有这样的几个问题。一是维度过高,它与整个vocabulary相关,因此维度也会随着词汇量的增加而增加;而是矩阵十分稀疏,这将导致难以训练出有效的模型。我们可以通过两种方法来降低词向量的维度:一是用数学方法对这个矩阵进行降维(如SVD奇异值分解);或是不通过这个矩阵,而是直接估计出词向量。

SVD奇异值分解

通过SVD分解,可以将一个mxn的矩阵分解为3个维度分别为rxn、rxr、mxr的矩阵。我们所需要的词向量信息都存储在第一个rxn的矩阵当中。第二个rxr的矩阵是一个对角矩阵,通过调整对角线上的数值可以对第一个rxn矩阵的列进行排序,从而让比较重要的维排在前面。

图片7

使用numpy可以很容易地进行SVD分解计算:

U, s, Vh = numpy.linalg.svd(X, full_matrices=False)

下图是取前两维生成的plot:

图片1

可以看出,相对相近的单词距离更近。

使用SVD方法时,可以进行一些hack,例如通过阈值或过滤的方法对停用词进行处理、添加权重、使用Pearson相关系数代替count等。

图片2

然而,SVD奇异值分解有一些弊端。例如,计算复杂度与m×n相关、难以加入新的单词等。最后,它与深度学习的审美不同:深度学习通常追求从某一个例子中学习一些东西然后再移动到下一个,而SVD方法却需要对所有语料进行整体地处理。

直接学习低维向量

除了利用SVD方法把高维矩阵降维处理外,还可以直接学习低维向量。例如word2vec,他直接预测每个单词相邻单词的概率。除了word2vec,2014年出现了Glove: Global Vectors for Word Representation. Pennington et al. (2014)。它们可以向语料库中不断添加新词。

word2vec的核心思想是:当窗口长度为c时,预测与中心词共现的单词。其目标函数为:

图片3

其中p为

图片4

然而当单词量增大后,其计算将会变得缓慢。因此,我们可以通过采取采样、简化概率函数等方式进行近似。

  基于频数的方法 直接预测方法
优势 训练速度快充分利用了统计信息

性能更好可以获得到比单词相似性更复杂的信息

劣势 主要用来获得单词之间的相似度当频数较少时,获得的比重不合适

与语料库的大小有关没有利用统计信息

 

Reference

http://cs224d.stanford.edu/

Jump Out of C++:面向对象编程探索(二)

这次是这一个系列技术分享的第二部分,主要介绍的是C++对象模型。这里所讲的对象模型,是指如何在内存中(空间上)表达对象的性质和特征,以及如何在运行时(时间上)提供完成面向对象系统中的必要操作的能力。

Table of Content


C++并不是天生地庞大而迟缓

—— Stanley B. Lippman

  1. 概述
  2. 对象的内存布局
  3. 运行期环境

1. 概述


对于传统的C语言程序来讲,数据与算法是分离的:

struct Circle  
{ 
int x, y;  
int radius;  
}; 

Circle MakeCircle(int x, int y, int radius) { //.... } void DrawCircle(Circle c) { //.... }  

在C++中,将这样的与传统C语言结构体内存布局兼容的对象叫做Plain Old Data(POD)。

此外,我们还可以将数据和算法整合在一起:

class Circle  
{ 
public:  
    Circle(int x, int y, int radius); 
    void Draw(); 
private:  
    int x, y; 
    int radius; 
};

对于这样的代码,在时空上是否增加了什么负担?同时,C++还支持继承、多重继承等很多机制,它们呢?

class Point {  
public:  
    Point(int x, int y); 
private:  
    int x, y; 
}; 
class Circle : public Point { //... };  

我们先从最朴素的模型开始,看一看如何来安排对象的内存空间:

  • 简单对象模型

在简单对象模型中,对象中只存储指向成员(包括数据成员和方法成员)的指针,所有操作均通过指针索引来完成。

  • (双)表格驱动模型

双表格驱动模型是指在对象中仅存储两个指针:一个指向数据成员table的指针,一个指向方法成员table的指针。这种模型的优点在于所有对象的内存空间占用及内存布局均完全一致。早期的某款C++编译器采用了这一种模型。

以上两种模型都是可以在一定程度上满足需求的,但并未被大多数编译器所采用。目前主流的模型如下:

  • 非静态数据存放于对象内部
  • 静态数据、非virtual函数存放于对象之外
  • 每一个class至少有一张虚函数表
  • 每一个object中包含一个指向该表的指针

1

本文将探讨在单继承、多继承、虚拟继承下C++对象模型是如何应对的。

2. 对象的内存布局


 2.1 类

对于如下的代码来说:

class Circle  
{ 
public:  
    Circle(int x, int y, int radius); 
    void Draw(); 
    static void Print(); 
private:  
    int x, y; 
    int radius; 
    static int count;
};

根据前述的对象模型,其其布局与在C语言中使用结构体时是一致的:

circle

同时C++标准保证,在同一个权限声明块中(即同一个public、protected或private块中),后声明的成员在内存中位于高地址(更靠后)。但并没有规定其一定紧邻,其间可以插入其他编译器所需的数据。

 2.2 不包含虚函数的单继承

而对于单继承来说:

class Point  
{ 
public:  
    Point(int x, int y); 
protected:  
    int x, y; 
}; 
class Circle : public Point { protected: int radius; };  

其内存布局如下:

绘图7

可以看出,其占用内存的成本并没有增加。然而是否一定不会增加呢?如果我们有如下代码:

class Point {  
public:  
    //... 
protected:  
    int x, y; 
    char c1, c2; 
}; 
class Circle : public Point {  
public:  
//... 
protected:  
    char c3, c4; 
    int radius; 
};

此时的内存布局将为:

绘图7_

由于需要对其内存,编译器很可能会插入padding字节。这时,继承的内存占用将会比数据全部在同一个类中的时候大。不过,我们是否可以取消掉padding呢?

答案自然是否定的。取消padding后,Point对象和Circle对象中Point部分在内存中占用的大小将会不同,此时如果存在以下代码:

void some_func(Point* point) { Point a; *point = a; }  

由于Circle和Point存在继承关系,我们不能在编译期确定代码中point的真实类型是什么。如果point一旦为Circle类型,那么在执行到*point=a这一句代码时,将会多覆盖两个字节的内存,即c3和c4将被内容不确定的内存覆盖。

2.3 包含虚函数的单继承

对于包含虚函数的单继承来说,其内存中将会增加一个指向虚表的指针。

class Point {  
public:  
    virtual void Print(); 
protected:  
    int x, y; 
}; 
class Circle {  
public:  
    virtual void Print();
protected:  
   int radius; 
};

circle<em>with</em>poly” title=”” /></a></p>
<p>需要说明的是,虚表指针_vptr可以在Point中的任意位置。上图中其位于Point对象的末尾,也可以位于最前,甚至在中间也可以。在末尾的好处在于其前面的部分仍然与C语言结构体兼容,在最前的好处是可以省去一步计算过程(对象的地址即为虚表指针的位置,不需要额外的计算)。而一般来讲没有编译器选择在中间位置插入虚表指针。</p>
<p>对于一个Point对象来讲,其<em>vptr将指向Point类的虚表;而对于一个Circle对象来讲,虽然图中标明了</em>vptr在Point部分中,但这仅表明它是由Point继承而来,其仍将指向Circle类的虚表,如下图所示:</p>
<p><a href=circle<em>with</em>poly_2″ title=”” /></a></p>
<h3 id=2.4 包含虚函数的多重继承

假如有如下代码:

class Point {  
public:  
    virtual void Print();
protected:  
    int x, y; 
}; 
class Text {  
public:  
    virtual void OutputText(); 
protected:  
    char buffer[4]; 
}; 
class Circle : public Point { protected: int radius; };  
class CircleWithText : public Circle, public Text  
{ protected: int text_x, text_y; };

在多重继承中,标准并未要求在内存中多个父类之间的顺序。一种可能的内存布局如下:

绘图1

从上图可以看出,此时CircleWithText中将存在两个虚表指针,分别从两条继承线路中继承而来。也就是说,此时CircleWithText类将包含两张虚表,一张中存放由Circle带来的虚函数,一张存放由Text带来的虚函数。假如CircleWithText自己还有新的虚函数,则可以择一存放。

与单继承相比,多重继承除了内存布局的复杂程度增加了之外,在几种情况下运行时的指针运算同样非常重要:

  • 指针的类型转换
  • 通过子类调用由第二个父类(内存中靠后的父类)继承而来的虚函数
  • 通过指向第二个父类(内存中靠后的父类)的指针调用被子类覆盖的虚函数

让我们逐一观察这几种情况。

1. 指针类型转换

CircleWithText cwt;  
Circle *pCircle = &cwt;  
Point *pPoint = &cwt; 

Text *pText = &cwt;  
//pText = (Text*)((char*)&cwt + sizeof(Circle));

对于前两条赋值语句来说,不需要额外的计算,因为在CircleWithText对象中,Circle部分和Point部分的起始地址与cwt一样。而如果要转换为第二个父类的指针,则需要一定的计算。在计算中应当对NULL进行判断,如果需要转换的指针是空指针的话,转换后的指针也应该为空指针。

2. 通过子类调用由第二个父类(内存中靠后的父类)继承而来的虚函数

CircleWithText cwt;  
CircleWithText *pcwt = &cwt;  
pcwt->OuputText();  

上述代码中,使用了子类指针pcwt调用了由Text父类继承而来的虚函数OutputText。由于我们需要传入this指针,这个指针不应当传入pcwt,而是pcwt中Text部分的起始地址。可是很可惜,这只是一种情况;我们无法在编译期确定究竟需要传入哪个this,因为很有可能存在第三种情况:

 3. 通过指向第二个父类(内存中靠后的父类)的指针调用被子类覆盖的虚函数

class CircleWithText : public Circle, public Text  
{ 
public:  
 virtual void OutputText(); 
}; 

CircleWithText cwt;  
Text *pText = &cwt;  
pText->OutputText();  

我们首先需要注意这一段代码与上段代码的不同。这段代码中CircleWithText覆盖了Text中的OutputText函数。此时在pText->OutputText()这一调用中,传入的应为cwt的起始地址。

关键的问题终于出现了:第一:如何确定应当传入哪一个this指针?第二,假设已经确定应当传入CircleWithText的起始地址,那么如何从pText指针重新找到CircleWithText的起始地址呢?在编译期,我们无法知晓这些问题,就如我们不知道下面代码中p的真实类型是什么:

void some_func(Text* p) { p->OutputText(); }  

因此,我们只能通过一定的运行时机制来解决这个问题。下面介绍两种可以解决这一问题的方法。

方案一:在虚函数表中同时存储函数地址和this指针的偏移

示例图如下:

mb_vftable

此时在虚表中同时存储函数地址和所需的this指针偏移(注意,是偏移而不是地址)。因为this指针的“地址”是跟每个对象相关的,而每一个类只有一张vtable。不过偏移却是对于一个类的所有对象都适用的(比如,在一个CircleWithText对象中,想把Text部分的起始地址转换为CircleWithText对象的起始地址所需的偏移是一定的)。这种方法的弊端在于增加了单继承时虚表的成本。

方案二:为函数添加thunk入口

mb_thunk

这种方法不改变虚表的结构,而是将原本指向成员函数的指针改为指向一段由编译器自动生成的thunk代码的地址。在这一小段代码中,对指针进行调整,然后再跳转到真正的函数中去。

2.5 虚拟继承

虚拟继承所要解决的问题时在继承树中同一个父类出现多次的情况。有时我们仅需要一个基类,这就需要虚拟继承来解决:

class Drawable { public: virtual void Draw(); };  
class Point : virtual public Drawable;  
class Text : virtual public Drawable;  

图片1

实现虚拟继承的难度在于,如何将两个Drawable对象变为一个,同时要解决指针计算的相关问题。下面介绍几种方案:

方案一:虚基类指针

在虚拟继承的子类中,额外存储一个指针用于指向直接虚继承的父类:

绘图2

然而这一方案的问题在于,如果有多层的虚拟继承,其间接访问的时间成本也将增加。如果存在多个虚继承父类,则需要多个指针。

方案二:对象的虚基类指针表

对于方案一的一种改进是将这些指针都存放在一张表里,通过一个指针来访问:

vbtable

这一方案解决了方案一中的问题。然而正如之前所说,“地址”是与对象相关的,如果采用了方案二,就是说每一个CircleWithText对象都需要两张虚基类的指针table。

方案三:在虚函数表的负索引处存放偏移

为了解决方案二中的弊端,可以考虑使用偏移,这样的话就不必为每一个对象都生成几个虚基类表了。同时,鉴于虚函数表的负索引还没有被利用,我们可以在这里存放虚基类的偏移。

vb_vftable

3. 运行时环境


3.1 运行时类型识别

在虚函数表的第0项中,并不存储函数指针,而是运行时类型信息type_info。dynamic_cast和typeid是与运行时类型相关的操作。

一个典型的type_info 类可能定义如下:

图片2

3.2 异常

对于如下代码,我们如何判断应当析构哪些对象?

图片3

一种方法是根据程序计数器PC,然而这可能与特定平台相关。另一种方案是使用一个标识变量step,在每一个对象构建或析构之后将step加1。同时存放一张表,用于查询当step为n时应当析构哪些对象。

2.3 C++与动态链接库

对于某一种C++编译器编译产生的动态链接库,将很难其与其他编译器一同使用。原因在于C++标准并没有规定在二进制上的实现方式,编译器在名称改编、成员布局以及异常等机制上均存在着差异。不过,使用抽象类可以一定程度上解决这个问题,详情可以参考一些面向对象系统的实现,例如COM。

感知器算法

这是我之前在实验室的机器学习讨论会上的分享。


Table of Contents

  • 人脑的启示
  • 感知器算法
  • 双层感知器
  • 反向传播算法
  • 应用

人脑的启示


人的神经是由一个个神经元组成的,包括树突、轴突、胞体等。

Picture1

人脑具有以下特征:

  • 超过100亿个神经元
  • 神经元响应时间:约1毫秒
  • 人脸识别:约0.1secs
  • 平均每个神经元与1000个神经元相连
  • 计算高度并行

人脑擅长于模式识别、联想,并可以容忍噪声;电脑则擅长于计算,罗辑清晰。人脑的操作速度很慢,并且运算的结果不可靠,但其可以大规模地并行计算;而电脑的运算速度相当快,并且结果非常可靠。

模拟神经元的结构和工作方式,是感知器算法的主要机制。例如,刺激程度决定了这个神经元的兴奋程度,并且与其他神经元连接紧密时,信号强度大;连接疏松时,信号强度弱。

感知器算法


感知器算法的原理非常简单:

感知器算法

首先对输入信号的求和,当超过阈值时即使感知器兴奋。算法描述如下(from A Course in Machine Learning):

感知器算法

从算法描述可以看出,学习的意义在于向正确的结果靠拢。两组同样的数据,第一次分错了,第二次将会更加接近正确结果。

MaxIter是需要人工指定的参数,如果MaxItger 过大,容易发生过拟合;过小则容易学不到什么东西 。

同时,整个学习的过程就是在一个平面上寻找判别边界。为了简化问题,假设有一个线性可分问题,b=0,则感知器算法即在寻找分界面B:

snapshot7

B就是虚线,而由wd组成的向量w垂直于B:

snapshot8

由此也可以得出,w的绝对大小并不重要。

感知器算法的特点有:

  1. 在线学习,不需要一次性考虑整个数据集

  2. 错误驱动,当分类正确时,不进行学习操作

  3. 对训练集的输入顺序敏感。例如,有10000条训练数据,前9000条数据在默认的参数上就分类正确,则此次迭代只有最后1000条数据得到了训练。因此,不断地调整训练数据的顺序将有助于加快训练速度:

Picture4

朴素的感知器算法仍然存在一定的缺陷,例如后训练的数据会更加重要。参考上面举得例子,如果有10000条训练数据,前100条数据已经训练出足够好的分类器,一直到最后一条之前全部分类正确。最后一条数据就会掉了一个在99.9%的数据上工作得都非常好的分类器。

从以上的分析也可以看出,单层的感知器算法只能用于解决线性可分的问题。

算法改进

投票感知器算法

投票感知器算法会在训练时记录每一个分类器的存活时间,在最终预测时根据权重进行投票。

Picture5

但是由于需要保存每一个超平面,并且预测时需要使用每一个分类器进行预测,时空开销将非常大。

平均感知器算法

平均感知器算法仅记录加权平均后的分类器:

Picture6

除此之外,还有像信任权感知器、被动主动感知器、权表决感知器等。信任权感知器通过对输入添加信任权重的概率分布,使得信任度大的输入会得到更大幅度的修正;被动主动感知器可以主动修正学习速率;权表决感知器会为每一维输入产生一个分类器,最终由虽有分类器结果汇总投票产生,并对错误的分类器予以惩罚。

双层感知器


 snapshot9

在双层感知器中,将有两层感知器需要训练。除此之外,在单层感知器中,我们使用的是线性函数来计算感知器是否被激活;这种用于判断感知器是否激活的函数叫做激活函数(link function)。双层感知器中,隐含层通常采用非线性的sigmoid(tanh)函数来作激活函数。

对于双层感知器来说,它可以模拟任意连续函数;隐含层节点越多,函数越复杂。如果隐含层层数再增加,则可以模拟任意函数。

反向传播算法


对于感知器计算输出的过程来说,相当于正向传播;而通过计算结果反馈给隐含层和输出层来调整参数,则属于反向传播。例如:

Picture1

我们可以通过输出结果与标准值的误差,来修正向量v和矩阵w。常用的方法是梯度下降法,目标函数为结果误差的几何均值。通过对目标函数f求w和v的偏导,就可以求出v和w的梯度。

目标函数:

Picture5

计算结果:

Picture3

Picture4

总结


优势

  • 数学基础 如果有线性解则可以找到(单层)
  • 可以容忍噪声
  • 可以处理任意连续/非连续函数(多层)
  • 可以学到复杂的输入输出映射关系

劣势

  • 知识表示不易解释
  • 依赖于hidden层激励函数
  • 输入输出的设计对结果具有影响
  • 参数较多(神经元数目、层数、学习速率等)
  • 可能需要使用者的经验
  • 训练时间长
  • 过拟合