部分NLP工程师面试题总结
阅读原文时间:2023年07月09日阅读:1

面试题

https://www.cnblogs.com/CheeseZH/p/11927577.html

其他

大数据相关面试题 https://www.cnblogs.com/CheeseZH/p/5283390.html

最大熵模型源码

# !/usr/bin/env python
# encoding: utf-8

"""
@Author  : hezhang
@Email   : hezhang@mobvoi.com
@Time    : 2019/10/15 下午10:26
@File    : max_ent.py
@IDE     : PyCharm
"""

from collections import defaultdict
import numpy as np

class maxEntropy(object):
    def __init__(self):
        self.trainset = []  # 训练数据集
        self.features = defaultdict(int)  # 用于获得(标签,特征)键值对
        self.labels = set([])  # 标签
        self.w = []

    def loadData(self, fName):
        for line in open(fName):
            fields = line.strip().split()
            # at least two columns
            if len(fields) < 2: continue  # 只有标签没用
            # the first column is label
            label = fields[0]
            self.labels.add(label)  # 获取label
            for f in set(fields[1:]):  # 对于每一个特征
                # (label,f) tuple is feature
                self.features[(label, f)] += 1  # 每提取一个(标签,特征)对,就自加1,统计该特征-标签对出现了多少次
            self.trainset.append(fields)
        # self.w = [0.0] * len(self.features)  # 初始化权重
        # self.lastw = self.w

    # 对于该问题,M是一个定值,所以delta有解析解
    def train(self, max_iter=10000):
        self.initP()  # 主要计算M以及联合分布在f上的期望
        # 下面计算条件分布及其期望,正式开始训练
        for i in range(max_iter):
            print("iter:",i)
            self.ep = self.EP() # 计算特诊函数在条件分布模型和边缘经验分布上的期望
            self.lastw = self.w[:]
            for i, w in enumerate(self.w):
                self.w[i] += (1.0 / self.M) * np.log(self.Ep_[i] / self.ep[i])
            if self.convergence():  # self.w 和 self.lastw的差值小于某个阈值
                break

    def initP(self):
        # 获得M, M表示所有特征出现的次数,应该是如下哪个值?不同值相当于给每个weight固定权重
        # 会影响最后分类概率的平滑性,但是不会影响大小顺序。取决于特征函数是二值函数,还是计数函数。
        self.M = max([len(feature[1:]) for feature in self.trainset])  # 通用迭代尺度算法
        # self.M = sum([len(feature[1:]) for feature in self.trainset])  # 特征函数是计数函数
        # self.M = len(self.features)  # 特征函数是二值函数
        self.N = len(self.trainset)
        self.Ep_ = [0.0] * len(self.features)
        # 获得联合概率期望
        for i, feat in enumerate(self.features):
            self.Ep_[i] += self.features[feat] / (1.0 * self.N)
            # 更改键值对为(label-feature)-->id
            self.features[feat] = i
        # 准备好权重
        self.w = [0.0] * len(self.features)
        self.lastw = self.w

    def EP(self):
        # 计算pyx
        ep = [0.0] * len(self.features)
        for record in self.trainset:
            features = record[1:]
            # cal pyx
            prob = self.calPyx(features)
            for f in features:  # 特征一个个来
                for pyx, label in prob:  # 获得条件概率与标签
                    if (label, f) in self.features:
                        id = self.features[(label, f)]
                        ep[id] += (1.0 / self.N) * pyx
        return ep

    # 获得最终单一样本每个特征的pyx
    def calPyx(self, features):
        # 传的feature是单个样本的
        wlpair = [(self.calSumP(features, label), label) for label in self.labels]
        # 归一化,预测的时候没必要
        Z = sum([w for w, l in wlpair])
        prob = [(w / Z, l) for w, l in wlpair]
        return prob

    def calSumP(self, features, label):
        sumP = 0.0
        # 对于这单个样本的feature来说,不存在于feature集合中的f=0所以要把存在的找出来计算
        for showedF in features:
            if (label, showedF) in self.features:
                sumP += self.w[self.features[(label, showedF)]]
        return np.exp(sumP)

    def convergence(self):
        for i in range(len(self.w)):
            if abs(self.w[i] - self.lastw[i]) >= 0.0001:
                return False
        print("convergence")
        return True

    def predict(self, input):
        features = input.strip().split()
        prob = self.calPyx(features)
        prob.sort(reverse=True)
        return prob

if __name__ == '__main__':
    mxEnt = maxEntropy()
    mxEnt.loadData('max_ent.train.data')
    mxEnt.train()
    for i, t in enumerate(mxEnt.features):
        print("{},{}".format(t, mxEnt.w[i]))
    print mxEnt.predict('Happy')

维特比算法源码

# !/usr/bin/env python
# encoding: utf-8

import numpy as np
# -*- codeing:utf-8 -*-

#   隐状态
hidden_state = ['sunny', 'rainy']

#   观测序列
# obsevition = ['walk', 'shop', 'clean']

#   根据观测序列、发射概率、状态转移矩阵、发射概率
#   返回最佳路径
def compute(obs, states, start_p, trans_p, emit_p):
    N = len(states)
    T = len(obs)

    #   max_p(T*2)每一列存储第一列不同隐状态的最大概率
    #   max_p[t][i], t时刻状态为i的所有单个路径中的概率最大值
    max_p = np.zeros((T, N))

    #   path(2*T)每一行存储上max_p对应列的路径
    #   path[t][i], t时刻状态为i的所有单个路径中的概率最大值对应的t-1时刻的状态???
    path = np.zeros((T, N))

    #   初始化
    for i in range(N):
        # t=0时,max_p = 初始概率 * 发射概率
        max_p[0][i] = start_p[i] * emit_p[i][obs[0]]
        # t=0时,没有上一状态,设置为自身
        path[0][i] = -1

    # 从t=1时刻开始遍历
    for t in range(1, T):
        # newpath = np.zeros((T, N))
        # 根据t-1时刻的max_p,计算t时刻每个可能状态的max_p
        for y in range(N):
            max_prob = -1
            pre_state = -1
            # y0表示t-1时刻的状态
            for y0 in range(N):
                nprob = max_p[t-1][y0] * trans_p[y0][y] * emit_p[y][obs[t]]
                if nprob > max_prob:
                    max_prob = nprob
                    pre_state = y0
            max_p[t][y] = max_prob
            path[t][y] = pre_state

    max_prob = -1
    end_state = 0
    #   返回最大概率的路径
    for y in range(N):
        if max_p[T-1][y] > max_prob:
            max_prob = max_p[T-1][y]
            end_state = y

    # res = [end_state]
    # print hidden_state[int(path[T-1][end_state])]
    for t in range(T-1, -1, -1):
        print "{}\t{}\t{}".format(t, end_state, hidden_state[int(path[t][end_state])])
        end_state = int(path[t][end_state])
    # print res
    # return res[::-1]

state_s = [0, 1]

obser = [0, 1, 2, 2, 1, 0]  #

#   初始状态,测试集中,0.6概率观测序列以sunny开始
start_probability = [0.6, 0.4]

#   转移概率,0.7:sunny下一天sunny的概率
transititon_probability = np.array([
    [0.7, 0.3],
    [0.4, 0.6]
])

#   发射概率,0.4:sunny在0.4概率下为shop
emission_probability = np.array([
    [0.1, 0.4, 0.5],
    [0.6, 0.3, 0.1]
])

result = compute(obser, state_s, start_probability, transititon_probability, emission_probability)

# for k in range(len(result)):
#     print(hidden_state[int(result[k])])

维特比算法2

# !/usr/bin/env python
# encoding: utf-8

"""
@Author  : hezhang
@Email   : hezhang@mobvoi.com
@Time    : 2019/10/22 下午6:04
@File    : viterbi.py
@IDE     : PyCharm
"""

MIN_FLOAT = -3.14e100
MIN_INF = float("-inf")

def viterbi(obs, states, start_p, trans_p, emit_p):
    V = [{}]  # tabular 表格
    mem_path = [{}]
    all_states = trans_p.keys()
    for y in states.get(obs[0], all_states):  # init
        V[0][y] = start_p[y] + emit_p[y].get(obs[0], MIN_FLOAT)
        mem_path[0][y] = ''
    for t in xrange(1, len(obs)):
        V.append({})
        mem_path.append({})
        #prev_states = get_top_states(V[t-1])
        prev_states = [
            x for x in mem_path[t - 1].keys() if len(trans_p[x]) > 0]

        prev_states_expect_next = set(
            (y for x in prev_states for y in trans_p[x].keys()))
        obs_states = set(
            states.get(obs[t], all_states)) & prev_states_expect_next

        if not obs_states:
            obs_states = prev_states_expect_next if prev_states_expect_next else all_states

        for y in obs_states:
            prob, state = max((V[t - 1][y0] + trans_p[y0].get(y, MIN_INF) +
                               emit_p[y].get(obs[t], MIN_FLOAT), y0) for y0 in prev_states)
            V[t][y] = prob
            mem_path[t][y] = state

    last = [(V[-1][y], y) for y in mem_path[-1].keys()]
    # if len(last)==0:
    #     print obs
    prob, state = max(last)

    route = [None] * len(obs)
    i = len(obs) - 1
    while i >= 0:
        route[i] = state
        state = mem_path[i][state]
        i -= 1
    return (prob, route)

softmax/交叉熵/dropout/Batch Norm/Layer Norm实现

import numpy as np
import tensorflow as tf
#a = tf.one_hot([1,2,3], 3)
#print(a)
def cross_entropy(predictions, targets, epsilon=1e-12):
    """
    Computes cross entropy between targets (encoded as one-hot vectors)
    and predictions.
    Input: predictions (N, k) ndarray
           targets (N, k) ndarray
    Returns: scalar
    """
    predictions = np.clip(predictions, epsilon, 1. - epsilon)
    N = predictions.shape[0]
    ce = - ( 1.0 / N ) * np.sum(targets*np.log(predictions))
    return ce

predictions = np.array([[0.25, 0.25, 0.25, 0.25],
                        [0.01, 0.01, 0.01, 0.96]])
targets = np.array([[0, 0, 0, 1],
                   [0, 0, 0, 1]])
ans = np.array([0.71355817782])  #Correct answer
x = np.array([cross_entropy(predictions, targets)])
print(np.isclose(x,ans))

##
# our NN's output  logits为输出层输出
logits = tf.constant([[1.0, 2.0, 3.0], [1.0, 2.0, 3.0], [1.0, 2.0, 3.0]])
# step1:do softmax
y = tf.nn.softmax(logits)
# true label  y_为训练集标签
y_ = tf.constant([[0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0]])
# step2:do cross_entropy    两种交叉熵实现方式
cross_entropy = -tf.reduce_sum(y_ * tf.log(y))
# do cross_entropy just one step
cross_entropy2 = tf.reduce_sum(
    tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=y_))  # dont forget tf.reduce_sum()!!

with tf.Session() as sess:
    softmax = sess.run(y)
    c_e = sess.run(cross_entropy)
    c_e2 = sess.run(cross_entropy2)
    print("step1:softmax result=")
    print(softmax)
    print("step2:cross_entropy result=")
    print(c_e)
    print("Function(softmax_cross_entropy_with_logits) result=")
    print(c_e2)


import numpy as np

def softmax(x):
    exp_x = np.exp(x)
    return exp_x / np.sum(exp_x)

# 在 numpy 中浮点类型是有数值上的限制的,对于float64,它的上限是 10^308。
# 对于指数函数来说,这个限制很容易就会被打破,如果这种情况发生了 python 便会返回 nan。
# 为了让 Softmax 函数在数值计算层面更加稳定,避免它的输出出现 nan这种情况,
# 一个很简单的方法就是对输入向量做一步归一化操作,仅仅需要在分子和分母上同乘一个常数C,
# 相当于e的指数加上log(C), 我们可以选择任意一个值作为log(C),但是一般我们会选择
# log(C)=max(X),通过这种方法就使得原本非常大的指数结果变成0,避免出现 nan的情况。

def stable_softmax(x):
    exp_x = np.exp(x - np.max(x))
    return exp_x / np.sum(exp_x)

x = [64, 32]
print(stable_softmax(x))
print(stable_softmax([i/8.0 for i in x]))

损失函数(MSE/交叉熵/Hinge)

激活函数(sigmoid/tanh)

梯度下降(GD/SGD/BGD/MBGD/Adagrad/RMSprop/Momentum/Adam)

随机数相关题目 https://octman.com/blog/2013-10-20-interview-questions-random-numbers/

# !/usr/bin/env python
# encoding: utf-8

# random choose k from N
import random
# print( random.randint(1,10) )        # 产生 1 到 10 的一个整数型随机数
# print( random.random() )             # 产生 0 到 1 之间的随机浮点数
# print( random.uniform(1.1,5.4) )     # 产生  1.1 到 5.4 之间的随机浮点数,区间可以不是整数
# print( random.choice('tomorrow') )   # 从序列中随机选取一个元素
# print( random.randrange(1,100,2) )   # 生成从1到100的间隔为2的随机整数

def reservior(k, N):
    pool = N[0:k]  # 如果只有k个,那全选
    for i in range(k, len(N)):
        # 再来新数据,则考虑替换
        m = random.randint(1, i)  # 理解1:从前1~i中随机选一个数删除,第m个数
                                  # 理解2:给新来的第i个数找个位置m
        if m < k:  # 如果第m个处于池子中,就替换成新元素
            pool[m] = N[i]

def shuffle(ls):
    l = len(ls)
    ls = list(ls)
    for i in range(l, -1, -1):
        j = random.randint(0, i)  # 将第j个数当作第i个数,第i个数就固定了,之后不动了
        if i != j:  # 如果不是原数组的第i个
            ls[i], ls[j] = ls[j], ls[i]

def random_choice(m, n, ls):
    # 从长度为n的数组ls中随机挑选m个数
    out = []
    for i, x in enumerate(ls):
        j = random.randint(0, n-i)  # 从n-i个数中随机挑1个数
        if j < m:  # 这个数是否需要放到篮子里
            out.append(x)
            m -= 1  # 篮子大小更新
    return out

def word_break(s, wordDict):
    if s == "":
        return True
    dp = [False for _ in range(len(s)+1)]
    dp[0] = True  # 空字符串一定是True
    for i in range(1, len(s)+1):
        for j in range(i):
            if dp[j] and s[j:i] in wordDict:  # dp[j]能够被拆分并且s[j:i]在词表里,表明dp[i]也满足要求
                dp[i] = True
                break
    return dp[-1]

牛客网-《剑指offer》-包含min函数的栈

牛客网-《剑指offer》-调整数组顺序使奇数位于偶数前面

牛客网-《剑指offer》-数值的整数次方[快速幂运算]

牛客网-《剑指offer》-二进制中1的个数

牛客网-《剑指offer》-矩形覆盖

牛客网-《剑指offer》-斐波那契数列

牛客网-《剑指offer》-跳台阶

牛客网-《剑指offer》-变态跳台阶

牛客网-《剑指offer》-旋转数组的最小数

牛客网-《剑指offer》-用两个栈实现队列

牛客网-《剑指offer》-从尾到头打印链表

牛客网-《剑指offer》-替换空格

牛客网-《剑指offer》-二维数组中的查找

牛客网-《剑指offer》-重建二叉树

编辑距离

最长公共子串

最长公共子序列

从无序数组中快速找到第k小的数

排序算法

字典树

class Trie:
    def __init__(self):
        self.dic={}

    def insert(self,strr):
        a = self.dic
        for i in strr:
            if not i in a:
                a[i] = {}
            a = a[i]
        a["end"] = True

    def search(self, strr):
        a = self.dic
        for i in strr:
            if not i in a:
                return False
            a = a[i]
        if "end" in a:
            return True
        else:
            return False

    def startsWith(self,strr):
        a = self.dic
        for i in strr:
            if not i in a:
                return False
            a = a[i]
        return True



1. 基础的数据结构:插入排序、选择排序 (记下时间复杂度), 链表新增、删除,二叉树的遍历,其他场景算法题大多出自leetcode
2. 逻辑回归损失是什么, 手动推导一遍
3. 对集成学习, SVM的理解,以公式的形式写出来最好
4. 对HMM ,CRF的理解, CRF的损失函数什么,维特比算法的过程
 5. 手写一个tfids
6. word2vec的CBOW与SkipGram模型及两种训练方式(负采样\层级softmax), 两种训练方式的区别和应用场景,
7. word2vec和fasttext的区别, 训练word2vec的有哪些重要参数
8. LSTM的单元结构图和6个公式要记住
9. 有几种Attention, Attention和self-Attention是具体怎么实现的,对应什么场景
10. BERT的模型架构,多少层,什么任务适合bert,什么任务不适合,应用在你写的项目改怎么做
11. tensorflow手写一个卷积代码, BILSTM + CRF模型的原理,记住常用基础api(比如jieba添加默认词典api,分词api)
12. 问项目阶段, 会问数据集怎么得到、模型的训练、怎么部署、项目人员周期,开发中出现问题怎么解决等

为什么bert att中要除以sqrt(d)?
当输入信息的维度d比较高,点积模型的值通常有比较大方差,从而导致softmax函数的梯度会比较小。因此,缩放点积模型可以较好地解决这一问题。

https://zhuanlan.zhihu.com/p/22447440
残差连接,为了解决梯度消失的问题,函数加了1个x,相当于给函数对x的导数增加了一个1。
何恺明等人经过一段时间的研究,认为极其深的深度网络可能深受梯度消失问题的困扰,BN、ReLU等手段对于这种程度的梯度消失缓解能力有限,并提出了单位映射的残差结构[7]。这种结构从本源上杜绝了梯度消失的问题:
基于反向传播法计算梯度优化的神经网络,由于反向传播求隐藏层梯度时利用了链式法则,梯度值会进行一系列的连乘,导致浅层隐藏层的梯度会出现剧烈的衰减,这也就是梯度消失问题的本源,这种问题对于Sigmoid激活尤为严重,故后来深度网络均使用ReLU激活来缓解这个问题,但即使是ReLU激活也无法避免由于网络单元本身输出的尺度关系,在极深度条件下成百上千次的连乘带来的梯度消失。

https://mp.weixin.qq.com/s/PzQoX07nglOW1I7Zp4QKLw
mean square error:适用于回归问题,mse=1/n*SUM((y_i-p_i)^2),和正确的预测,错误的预测都有关
cross entropy: 适用于分类问题, ce = -(SUM(s_i*log(p_i))), s_i是one-hot形式的用于选择正确类别的概率,也就是只和正确的结果有关,和错误的无关;
    全连接》softmax》cross entropy
区别1:当模型预测正确时,mse和ce的loss都会趋近于0,但是当模型预测错误的时候,ce会给模型更大的惩罚,也就是更大的梯度
区别2:当激活函数是sigmoid或softmax这种两端有饱和区的函数,用mse的时候梯度和激活函数成正比,当处于饱和区的时候,
    mse容易出现梯度消失的情况,而用ce的时候,梯度和激活函数值&实际值的差成正比,也就是误差越大,梯度越大。

sigmoid:f(x) = 1/(1+e^(-x)), 呈S型;求导 f'(x) = f(x)(1-f(x)), 呈山丘型,两边接近0,导致梯度更新缓慢 ,最大值1/4
softmax: e_x/SUM(e_i)
hinge损失:2分类,SVM
    - max(0, 1-ty), t={-1, 1}, -1负样本,1正样本,y实数预测值

梯度下降(GD)取决于学习率(步长)和梯度(方向)
    SGD
    BGD
    MBGD
Adagrad:更新学习率时考虑历史所有梯度,累加之前所有的梯度平方。
    缺点:在训练的中后期,分母上梯度平方的累加将会越来越大,从而学习率趋近于0,使得训练提前结束。
Momentum:更新梯度时考虑历史所有梯度向量(包括方向和大小)
RMSprop:更新学习率时考虑历史所有梯度
    RMSprop是计算之前所有梯度的平均值,因此可缓解Adagrad算法学习率下降较快的问题。
Adam:同时调整学习率和梯度
    Beta1:0.9,用于调整历史方向的权重,Momentum
    Beta2: 0.99,用于调整历史步长的权重,RMSprop
    Epsilon:1e-08,平滑因子,防止分母为0

L1正则化:求导之后正负不同,大小固定。
L2正则化:求导之后考虑了大小。

逻辑回归
    - 二分类:sigmoid
    - 多分类:softmax
线性回归
    多元一次方程

激活函数
    sigmoid:1/(1+e^-x)
        - x偏大/偏小,梯度变化不明显,梯度消失
        - exp计算成本高
        - 区间范围0~1,更新同一层的参数,只能向同一个方向,导致梯度Z型问题
        - 导数最大1/4
    tanh
        - 区间范围-1~1
        - 也存在梯度消失的问题
        - 导数小于1
    relu
        - 前向过程x<0,则神经元处于非激活状态,反向过程没有梯度,神经元进入死区。
    leaky-relu
        - 小于0时:x/a
    elu
        - 小于0时:a(e^x-1), 随着x变小,趋于饱和
    gelu
        - 在elu的基础上增加了随机正则,tf实现:
        def gelu(input_tensor):
            cdf = 0.5 * (1.0 + tf.erf(input_tensor / tf.sqrt(2.0)))
            return input_tesnsor*cdf

初始化
    - 0值初始化,网络退化成1个神经元
    - 随机初始化,对于较小的值,在反向传播过程中梯度会被削弱,导致网络无法学习
    - Xavier,针对tanh激活函数
    - He,针对relu激活函数
Batch Normalization:
    - 解决:Internal Covariate Shift
    - x = gama*(x-E(x))/sqrt(D(x)) + beta
    - 经过归一化后,输入向量的每个维度的均值为 0,方差为 1
    - 引入 Batch Normalization 之后可以加大学习率,防止了权重参数上较小的变化放大为激活函数上梯度较大的次优的变化
    - 通常,大的学习率会增加每层权重参数的大小,权值参数大小的增加进一步在反向传播过程中加大了梯度并导致梯度爆炸。然而,通过 Batch Normalization,反向传播时,将不收权重参数的大小的影响
    - 不适用于RNN:对于有固定深度的前向网络来说,对每一层分别存储各自的上述两个统计量是没有问题的,然而对于循环神经网络来说,我们需要计算和保存序列中不同时间步骤的上述两个统计量,若测试序列比所有训练序列均要长,则 Batch Normalization 将会遇到问题。Batch Normalization 无法应用到在线学习任务或者非常大的分布式模型任务上,此时训练的 batch 较小,因此基于较小的 batch 的计算出的均值和方差统计量无法有效表示全局样本的统计量。
Layer Normalization:
    - 假设在非线性激活前的输入的随机变量的分布接近,因此可以直接基于每层的所有非线性激活前的输入估计均值和方差,并基于这两个统计量对输入进行均值为 0,方差为 1 的归一化。
    - 每层表示1个样例某个时刻的所有特征的值,将身高/体重/年龄统一归一化。
    - Layer Normalization 假设在非线性激活前的输入的随机变量的分布接近,而 CNN网络中图像边缘对应的 kernel 由于由大量的隐藏单元未被激活,因此该假设不成立,因此在 CNN 网络中 Layzer Normalization 效果没有 Batch Normalization 效果好。

    crf++
        - 模版的含义
            U04:%x[2,0]
            U05:%x[-1,0]/%x[0,0]
        - 参数的含义
            - c=1.2:惩罚因子,c越大,越容易过拟合
            - f=1:特征频数阈值

    liblinear速度更快,适合线上使用
        - L1/L2正则化
        - L1/L2损失
        - SVM/Logistic Regression
        - one-vs-the rest
        - 线性SVM

    SVM细节
    word2vec训练
    x 信息赠一
    x 信息赠一笔
    SMP细节
    x 决策树生成&剪枝
    python
    java
    数据结构
        链表
        树
        排序
        查找

单例模式

# 1. 使用模块
class MySingleton(object):
    def func(self):
        print("my singleton")

my_singleton = MySingleton()

# from single import my_singleton
# my_singleton.func()

# 2. 使用__new__
# new和init:https://stackoverflow.com/questions/674304/why-is-init-always-called-after-new

class MySingleton2(object):
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(MySingleton2, cls).__new__(cls)
        return cls._instance

class MyClass(MySingleton2):
    def func(self):
        print("singleton with __new__")

a = MyClass()
b = MyClass()
print(id(a) == id(b))

# 3. 使用类装饰器
from functools import wraps

def singleton(cls):
    instances = {}
    @wraps(cls)
    def get_instance(*args, **kw):
        if cls not in instances:
            instances[cls] = cls(*args, **kw)
        return instances[cls]
    return get_instance

@singleton
class MyClass2(object):
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def func(self):
        print("singleton with decorator")
a = MyClass2(1, 2)
b = MyClass2(2, 3)

print a.b
print b.b
print(a is b)