SparkMl-BucketedRandomProjectionLSH(欧几里德距离度量-局部敏感哈希)

LSH(Locality Sensitive Hashing)翻译成中文,叫做“局部敏感哈希”,它是一种针对海量高维数据的快速最近邻查找算法。可以用于推荐,例如用某用户的历史浏览信息,做信息推荐

BucketedRandomProjectionLSH是一个Estimator。我们在做大规模文本相似度计算的时候我们可能会用的模型,当然除了它之外还有一个MinHashLSH。MinHash 是一个用于Jaccard 距离的 LSH family。而BucketedRandomProjectionLSH是对MinHashLSH的一种优化,增加了的桶的概念,来降低整个计算的负责度。

参数信息参数描述备注
setInputColDF中待变换的特征特征类型必须为:vector
setOutputCol变换后的特征名称
setBucketLength每个哈希桶的长度必填
setNumHashTables哈希表的数量默认1
setSeed随机种子随机数生成使用,默认:772209414
approxSimilarityJoin(datasetA, datasetB, threshold)过滤模型分析数据集A与B中距离大于等于threshold数据
approxNearestNeighbors(datasetA, key, numNearestNeighbors)Spark的LSH的输出TopK
注:模型的输入特征可以是连续特征也可以是离散特征

离散特征代码示例

//特征名称
var features = Array("weight", "height", "age")
//字段转换成特征向量
var splitDatas = new VectorAssembler()
.setInputCols(features)
.setOutputCol("vector_features")
.transform(dataFrame.select("id", features:_*))
.randomSplit(Array(0.4, 0.3, 0.3))
//训练模型
var model:BucketedRandomProjectionLSHModel = new BucketedRandomProjectionLSH()
.setInputCol("vector_features")         //待变换的特征
.setOutputCol("bkt_lsh")                //变换后的特征名称
.setBucketLength(10d)                   //每个哈希桶的长度,更大的桶降低了假阴性率
.setNumHashTables(5)                    //哈希表的数量,散列表数量的增加降低了错误的否定率,如果降低它的值则会提高运行性能
.setSeed(100L)                          //随机种子
.fit(splitDatas.apply(0))               //训练
//通过模型转换数据
var transform = model.transform(splitDatas.apply(0))
transform.show(10, 100)
transform.printSchema()
//推荐信息,获取相关性较高的数据
var recommend= model.approxSimilarityJoin(splitDatas.apply(1), splitDatas.apply(2), 2, "distCol")
.select(
    col("datasetA").getField("id").as("id"), 
    col("datasetB").getField("id").as("recommend_id"), 
    col("datasetA").getField("age").as("age"), 
    col("datasetB").getField("age").as("recommend_age"), 
    col("datasetA").getField("weight").as("weight"), 
    col("datasetB").getField("weight").as("recommend_weight"), 
    col("datasetA").getField("height").as("height"), 
    col("datasetB").getField("height").as("recommend_height"),
    col("distCol")
)
recommend.orderBy("id", "distCol").show(100, 1000)

连续特征代码示例

	// df 数据集是dataframe,并且words字段是格式是 ["我","爱","北京","天安门"]的词列表Array[String]
    val word2Vec = new Word2Vec()
	  .setInputCol("words")
	  .setOutputCol("wordvec")
	  .setVectorSize(10)
	  .setMinCount(0)
	val wvModel = word2Vec.fit(df)
	val w2vDf = wvModel.transform(df)

	val brp = new BucketedRandomProjectionLSH()
	  .setBucketLength(4d)
	  .setNumHashTables(10)
	  .setInputCol("wordvec")
	  .setOutputCol("hashes")
	val brpModel = brp.fit(w2vDf)
	val tsDf = brpModel.transform(w2vDf)

	val key_value = List(0.13868775751827092,-0.11639275898904025,0.19808808788014898,0.2722799372859299,0.08275626220836721,-0.2846828463712129,0.2887565325463897,-0.05958885527697617,0.042977130971848965,-0.03787828763497287)
	val key = Vectors.dense(key_value.toArray)
	val a = brpModel.approxNearestNeighbors(tsDf, key , 3).toDF()

敲黑板:
LSH模型的使用中最关键的还是要看setBucketLength和setNumHashTables两个参数的设置。因为这两个参数决定了模型的计算效果和性能。当然这之前的输入数据结构也很重要,我们抛开输入数据的差异性不说来谈这两个参数。总的来说这两个参数的数值尽可能小一些,这样性能会高,但是推荐结果可能准确率低一些,这本身是一个博弈的过程。

缺点们:

  • 计算不稳定:Spark的LSH动不动卡着不动或者慢或者OOM,主要原因是join步骤相当消耗资源和桶内数据倾斜导致,然而在倾斜的桶内暴力搜索可能是不值得的,因为相似度数据对可能也在另一个不倾斜的桶内出现了
  • 数据丢失:调用approxSimilarityJoin会莫名其妙的丢失实体,比如输入1000个实体做最近邻50个检索,最后只输出了200个实体的top50,这个问题不是半径太小导致的,而是哈希之后没有任何一条(hash_table,哈希值)一样可以join上的数据对,这个问题是参数设置导致的,LSH调参比较蛋疼
  • 不能对所有实体输出TopK:Spark的LSH的approxNearestNeighbors是输出TopK,和需求完全切合,但是这个API不支持在全表操作,只能输出一个实体进行推荐,所以只能使用join方法再对join到的数据对进行排序取topK,相当浪费计算资源
  • 不支持余弦相似度:Spark的BucketedRandomProjectionLSH不支持余弦相似度,这个影响不大,可以先做一步归一化然后用欧氏距离,但是不是很方便

Python 之远程部署利器Fabric

假设一个场景:你手里有一个项目需要多节点部署,使之负载均衡,但是公司又没有采用部署工具,只是给你了56789…台机器。如果快速的把一份代码快速的部署到所有的机器上?

Fabric 是一个Python的库,它提供了丰富的同SSH交互的接口,可以用来在本地或远程机器上自动化、流水化地执行Shell命令。因此它非常适合用来做应用的远程部署及系统维护。其上手也极其简单,你需要的只是懂得基本的Shell命令。本文将为大家详细介绍Fabric的使用。

安装Fabric

首先Python的版本必须是2.7以上,可以通过下面的命令查看当前Python的版本: python -V

Fabric的官网是www.fabfile.org,源码托管在Github上。你可以clone源码到本地,然后通过下面的命令来安装。

https://docs.fabfile.org/en/1.10/usage/env.html

python setup.py develop

在执行源码安装前,你必须先将Fabric的依赖包Paramiko装上。所以,个人还是推荐使用pip安装,只需一条命令即可:

pip install fabric

第一个例子

万事从Hello World开始,我们创建一个”fabfile.py”文件,然后写个hello函数:

def hello():
  print("Hello Fabric!")

在”fabfile.py”的目录下执行命令:fab hello

你可以在终端看到”Hello Fabric!”字样。

简单解释下,”fabfile.py”文件中每个函数就是一个任务,任务名即函数名,上例中是”hello”。”fab”命令就是用来执行”fabfile.py”中定义的任务,它必须显式地指定任务名。你可以使用参数”-l”来列出当前”fabfile.py”文件中定义了哪些任务:fab -l

任务可以带参数,比如我们将hello函数改为:

def hello(name, value):
 print("Hello Fabric! %s=%s" % (name,value))

此时执行hello任务时,就要传入参数值:

fab hello:name=Year,value=2016

Fabric的脚本建议写在”fabfile.py”文件中,如果你想换文件名,那就要在”fab”命令中用”-f”指定。比如我们将脚本放在”script.py”中,就要执行:

fab -f script.py hello

执行本地命令

“fabric.api”包里的”local()”方法可以用来执行本地Shell命令,比如让我们列出本地”/home/bjhee”目录下的所有文件及目录:

from fabric.api import local
 
def hello():
 local('ls -l /home/bjhee/')

“local()”方法有一个”capture”参数用来捕获标准输出,比如:

def hello():
 output = local('echo Hello', capture=True)

这样,Hello字样不会输出到屏幕上,而是保存在变量output里。”capture”参数的默认值是False。

执行远程命令

Fabric真正强大之处不是在执行本地命令,而是可以方便的执行远程机器上的Shell命令。它通过SSH实现,你需要的是在脚本中配置远程机器地址及登录信息:

from fabric.api import run, env
 
env.hosts = ['example1.com', 'example2.com']
env.user = 'bjhee'
env.password = '111111'
 
def hello():
 run('ls -l /home/bjhee/')

“fabric.api”包里的”run()”方法可以用来执行远程Shell命令。上面的任务会分别到两台服务器”example1.com”和”example2.com”上执行”ls -l /home/bjhee/”命令。这里假设两台服务器的用户名都是”bjhee”,密码都是6个1。你也可以把用户直接写在hosts里,比如:

env.hosts = ['[email protected]', '[email protected]']

如果你的”env.hosts”里没有配置某个服务器,但是你又想在这个服务器上执行任务,你可以在命令行中通过”-H”指定远程服务器地址,多个服务器地址用逗号分隔:

fab -H [email protected],[email protected] hello

另外,多台机器的任务是串行执行的,关于并行任务的执行我们在之后会介绍。如果对于不同的服务器,我们想执行不同的任务,上面的方法似乎做不到,那怎么办?我们要对服务器定义角色:

from fabric.api import env, roles, run, execute, cd
 
env.roledefs = {
 'staging': ['[email protected]','[email protected]'],
 'build': ['[email protected]']
}
 
env.passwords = {
 'staging': '11111',
 'build': '123456'
}
 
@roles('build')
def build():
 with cd('/home/build/myapp/'):
 run('git pull')
 run('python setup.py')
 
@roles('staging')
def deploy():
 run('tar xfz /tmp/myapp.tar.gz')
 run('cp /tmp/myapp /home/bjhee/www/')
 
def task():
 execute(build)
 execute(deploy)

这时Fabric会先在一台build服务器上执行build任务,然后在两台staging服务器上分别执行deploy任务。”@roles”装饰器指定了它所装饰的任务会被哪个角色的服务器执行。如果某一任务上没有指定某个角色,但是你又想让这个角色的服务器也能运行该任务,你可以通过”-R”来指定角色名,多个角色用逗号分隔:

fab -R build deploy

这样”build”和”staging”角色的服务器都会运行”deploy”任务了。注:”staging”是装饰器默认的,因此不用通过”-R”指定。此外,上面的例子中,服务器的登录密码都是明文写在脚本里的。这样做不安全,推荐的方式是设置SSH自动登录,具体方法大家可以去网上搜搜。

SSH功能函数

到目前为止,我们介绍了”local()”和”run()”函数分别用来执行本地和远程Shell命令。Fabric还提供了其他丰富的功能函数来辅助执行命令,这里我们介绍几个常用的:

  • sudo: 以超级用户权限执行远程命令

功能类似于”run()”方法,区别是它相当于在Shell命令前加上了”sudo”,所以拥有超级用户的权限。使用此功能前,你需要将你的用户设为sudoer,而且无需输密码。

from fabric.api import env, sudo
 
env.hosts = ['[email protected]', '[email protected]']
env.password = '111111'
 
def hello():
 sudo('mkdir /var/www/myapp')
  • get(remote, local): 从远程机器上下载文件到本地

它的工作原理是基于scp命令,使用的方法如下:任务将远程机上”/var/log/myapp.log”文件下载到本地当前目录,并命名为”myapp-0301.log”

from fabric.api import env, get
 
env.hosts = ['[email protected]',]
env.password = '111111'
 
def hello():
 get('/var/log/myapp.log', 'myapp-0301.log')
  • put(local, remote): 从本地上传文件到远程机器上

同get一样,put方法也是基于scp命令,使用的方法如下:上述任务将本地”/tmp/myapp-0301.tar.gz”文件分别上传到两台远程机的”/var/www/”目录下,并命名为”myapp.tar.gz”。如果远程机上的目录需要超级用户权限才能放文件,可以在”put()”方法里加上”use_sudo”参数:

from fabric.api import env, put
 
env.hosts = ['[email protected]', '[email protected]']
env.password = '111111'
 
def hello():
 put('/tmp/myapp-0301.tar.gz', '/var/www/myapp.tar.gz')
put('/tmp/myapp-0301.tar.gz', '/var/www/myapp.tar.gz', use_sudo=True)
  • prompt: 提示输入

该方法类似于Shell中的”read”命令,它会在终端显示一段文字来提示用户输入,并将用户的输入保存在变量里:

from fabric.api import env, get, prompt
 
env.hosts = ['[email protected]',]
env.password = '111111'
 
def hello():
 filename = prompt('Please input file name: ')
 get('/var/log/myapp.log', '%s.log' % filename)

现在下载后的文件名将由用户的输入来决定。我们还可以对用户输入给出默认值及类型检查:如果你直接按回车,则port变量即为默认值8080;如果你输入字符串,终端会提醒你类型验证失败,让你重新输入,直到正确为止。

port = prompt('Please input port number: ', default=8080, validate=int)
  • reboot: 重启服务器

看方法名就猜到了,有时候安装好环境后,需要重启服务器,这时就要用到”reboot()”方法,你可以用”wait”参数来控制其等待多少秒后重启,没有此参数则代表立即重启:estart任务将在一分钟后重启服务器。

from fabric.api import env, reboot
 
env.hosts = ['[email protected]',]
env.password = '111111'
 
def restart():
 reboot(wait=60)

上下文管理器

Fabric的上下文管理器是一系列与Python的”with”语句配合使用的方法,它可以在”with”语句块内设置当前工作环境的上下文。让我们介绍几个常用的:

  • cd: 设置远程机器的当前工作目录

“cd()”方法在之前的范例中出现过,”with cd()”语句块可以用来设置远程机的工作目录:例中的文件会上传到远程机的”/var/www/”目录下。出了”with cd()”语句块后,工作目录就回到初始的状态,也就是”bjhee”用户的根目录。

from fabric.api import env, cd, put
 
env.hosts = ['[email protected]', ]
env.password = '111111'
 
def hello():
 with cd('/var/www/'):
 put('/tmp/myapp-0301.tar.gz', 'myapp.tar.gz')
  • lcd: 设置本地工作目录

“lcd()”就是”local cd”的意思,用法同”cd()”一样,区别是它设置的是本地的工作目录:

from fabric.api import env, cd, lcd, put
 
env.hosts = ['[email protected]', ]
env.password = '111111'
 
def hello():
 with cd('/var/www/'):
 with lcd('/tmp/'):
 put('myapp-0301.tar.gz', 'myapp.tar.gz')
  • path: 添加远程机的PATH路径

假设我们的PATH环境变量默认是”/sbin:/bin”,在上述”with path()”语句块内PATH变量将变为”/sbin:/bin:/home/bjhee/tmp”。出了with语句块后,PATH又回到原来的值。

from fabric.api import env, run, path
 
env.hosts = ['[email protected]', ]
env.password = '111111'
 
def hello():
 with path('/home/bjhee/tmp'):
 run('echo $PATH')
 run('echo $PATH')
  • settings: 设置Fabric环境变量参数

Fabric环境变量即是我们例子中一直出现的”fabric.api.env”,它支持的参数可以从官方文档中查到.我们将环境参数”warn_only”暂时设为True,这样遇到错误时任务不会退出。

from fabric.api import env, run, settings
 
env.hosts = ['[email protected]', ]
env.password = '111111'
 
def hello():
 with settings(warn_only=True):
 run('echo $USER')
  • shell_env: 设置Shell环境变量

可以用来临时设置远程和本地机上Shell的环境变量

from fabric.api import env, run, local, shell_env
 
env.hosts = ['[email protected]', ]
env.password = '111111'
 
def hello():
 with shell_env(JAVA_HOME='/opt/java'):
 run('echo $JAVA_HOME')
 local('echo $JAVA_HOME')
  • prefix: 设置命令执行前缀
from fabric.api import env, run, local, prefix
 
env.hosts = ['[email protected]', ]
env.password = '111111'
 
def hello():
 with prefix('echo Hi'):
 run('pwd')
 local('pwd')

在上述”with prefix()”语句块内,所有的”run()”或”local()”方法的执行都会加上”echo Hi && “前缀,也就是效果等同于:

run('echo Hi && pwd')
local('echo Hi && pwd')

MacBook–配置JDK[m1]

系统比较老或者Mac本身比较旧的情况,随便搜一下教程即可安装,主要就是几个方式,一是通过brew 安装,或者是下载JDK按照安装步骤一步一步安装就好了,我们的这个教程对应的Mac系统版本信息如下:

MacBook Pro (13-inch, M1, 2020)
芯片:Apple M1

系统本身自带了openjdk的java环境,如果系统自带的版本对于我们要应用的项目不适用的话就需要从新安装环境,而安装的方式依旧是那三板斧。

方式一: brew安装; 访达->应用程序-> 实用工具->终端。 打开终端输入命令安装即可。

通过brew install 直接安装所需要的Java 版本即可,但是教新版本的Mac系统在通过brew安装的时候会有一些问题,比如直接 brew install java8 可能会报错,在或者直接 brew install –cask java8 依旧会报错,这个时候需要改变一下命令行的格式。brew install –cask homebrew/cask-versions/java8

方式二:直接官方下载需要的JDK版本

dk下载的地址是:https://www.oracle.com/technetwork/java/javase/downloads/index.html

注意:下载是在Oracle官方地址下载,需要账号下载,如果没有账号随便注册一个即可。

安装完之后我们的Mac至少有两个版本的java。查看默认的java版本通过在终端输入命令:java — version

openjdk 16.0.1 2021-04-20
OpenJDK Runtime Environment (build 16.0.1+9-24)
OpenJDK 64-Bit Server VM (build 16.0.1+9-24, mixed mode, sharing)

如上,输入了java –version之后我们得到的输出可能还是系统默认的java版本,而我们自己安装的java没有成为默认版本(这个情况一般出现在第二种下载安装的方式后)。

如何修改Mac默认java版本?

首先我们得先知道我们的机器上一共有几种java版本才可以,知己知彼才能事倍功半,现在的问题就转到了

如何查看Mac安装了几个版本的java? Mac的java默认安装路径在哪?

打开终端,执行     /usr/libexec/java_home -V

MacBook-Air:~ eng$ /usr/libexec/java_home -V
Matching Java Virtual Machines (4):
    1.8.0_101, x86_64:  “Java SE 8”     /Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home
    1.7.0_79, x86_64:   “Java SE 7”     /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home
    1.6.0_65-b14-466.1, x86_64: “Java SE 6”     /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
    1.6.0_65-b14-466.1, i386:   “Java SE 6”     /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home

默认JDK1.6(Apple自带JDK)路径:   /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home

默认JDK1.7、1.8(Oracle) Home :  /Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home

小小历史课:

过去 Mac 上的 Java 都是由 Apple 自己提供的,只支持到 Java 6,并且OS X 10.7 开始系统并不自带(而是可选安装)。后来 Apple 加入 OpenJDK 继续支持 Java 6,而 Java 7 将由 Oracle 负责提供。

根据苹果的官方说明。Mac OS X 10.5 及以后的版本应该使用 /usr/libexec/java_home 命令来确定 JAVA_HOME ,而在此之前的版本由于没有这个命令,则应该使用固定的 /Library/Java/Home 目录。

最后的最后:

我们来说一下如何修改默认的java版本。很多教程会告诉你在终端输入 : cd 然后回车,直接跳到用户空间的根目录下,一般是在你电脑名字的目录里,你可以通过pwd 命令查看你当前的所在目录。

然后教程也一般会说编辑文件 .bash_profile (如何文件没有直接创建即可),但是如果Mac是新版本就会发现,这个文件基本上没有的,当然按照着教程的说法直接创建也是可以的。不过所有人都忽略一个事实一般,为啥新系统的Mac没有这个文件? 是不是有新的替代文件,答案是有替代文件。

在用户空间根目录下(如何不知道这个目录是哪里,就把终端全部关掉,然后再从新打开一个终端,那么打开的新终端所在的目录就是用户空间根目录)。输入命令: ls -thla

这个ls 命令是Linux基本操作命令,不做进一步说明解释。

命令输入后我们会在一堆名字中看到一个 .zshrc 的文件。我们直接。

vim .zshrc

把教程里说的添加 export JAVA_HOME=/path/to/java/dir(你java安装的目录),保存退出就可以了。