The problem is neither in the PIL nor in your code you are doing it right, but bear in mind while working with font rendering in images.
Your Problem
- It is very complex problem, implemented many times, improved many times still its a very huge research topic.
 
- PIL uses its own font rendering engine, and "Microsoft YaHei" means you are running code in windows most probably & it has it's own rendering engine its just an assumption.
 
- This change of implementation in both PIL & Windows will always makes some difference does not matter if you provide same arguments to both of these.
 
The Solution
Use Win32API in python to use windows rendering engine in python to
obtain almost same results or at least nearest.
 
Or try to experiment with gamming library pygame, as it is made to develop
games so it will handle font rendering more precisely and using a
sophisticated way to achieve reliability.
 
Using Windows API
import ctypes
import struct
import win32con
import win32gui
import win32ui
from PIL import Image
def RGB(r, g, b):    
    return r | (g << 8) | (b << 16)
def native_bmp_to_pil(hdc, bitmap_handle, width, height):
    bmpheader = struct.pack("LHHHH", struct.calcsize("LHHHH"),
                            width, height, 1, 24) #w,h, planes=1, bitcount)
    c_bmpheader = ctypes.c_buffer(bmpheader)
    #3 bytes per pixel, pad lines to 4 bytes    
    c_bits = ctypes.c_buffer(" " * (height * ((width*3 + 3) & -4)))
    res = ctypes.windll.gdi32.GetDIBits(
        hdc, bitmap_handle, 0, height,
        c_bits, c_bmpheader,
        win32con.DIB_RGB_COLORS)
    if not res:
        raise IOError("native_bmp_to_pil failed: GetDIBits")
    im = Image.frombuffer(
        "RGB", (width, height), c_bits,
        "raw", "BGR", (width*3 + 3) & -4, -1)
    return im    
class Win32Font:
    def __init__(self, name, height, weight=win32con.FW_NORMAL,
                 italic=False, underline=False):
        self.font = win32ui.CreateFont({
            'name': name, 'height': height,
            'weight': weight, 'italic': italic, 'underline': underline})
        #create a compatible DC we can use to draw:
        self.desktopHwnd = win32gui.GetDesktopWindow()
        self.desktopDC = win32gui.GetWindowDC(self.desktopHwnd)
        self.mfcDC = win32ui.CreateDCFromHandle(self.desktopDC)         
        self.drawDC = self.mfcDC.CreateCompatibleDC()
        #initialize it
        self.drawDC.SelectObject(self.font)
    def renderText(self, text):
        """render text to a PIL image using the windows API."""
        self.drawDC.SetTextColor(RGB(255,0,0))
        #create the compatible bitmap:
        w,h = self.drawDC.GetTextExtent(text)
        
        saveBitMap = win32ui.CreateBitmap()
        saveBitMap.CreateCompatibleBitmap(self.mfcDC, w, h)        
        self.drawDC.SelectObject(saveBitMap)
        #draw it
        self.drawDC.DrawText(text, (0, 0, w, h), win32con.DT_LEFT)
        #convert to PIL image
        im = native_bmp_to_pil(self.drawDC.GetSafeHdc(), saveBitMap.GetHandle(), w, h)
        #clean-up
        win32gui.DeleteObject(saveBitMap.GetHandle())
        return im        
    def __del__(self):
        self.mfcDC.DeleteDC()
        self.drawDC.DeleteDC()
        win32gui.ReleaseDC(self.desktopHwnd, self.desktopDC)
        win32gui.DeleteObject(self.font.GetSafeHandle())
    def __del__(self):
        win32gui.DeleteObject(self.font.GetSafeHandle())
and to use that
f = Win32Font("Your Font Name e.g. Microsoft YaHei", 15) #to use your font file install the font in windows
im = f.renderText("your text") #render your text
im.save("/path/to/image") #save your image if needed
Using Pygame
pygfont = pygame.font.Font(r"c:\windows\fonts\yourcustomfont.ttf", 15)
surf = pygfont.render("your text to render", False, (0,0,0), (255,255,255)) #False means anti aliasing disabled, you can experiment with enabled flag also
pygame.image.save(surf, r"/path/to/your/image")
To install pygame run pip install pygame for python2 or pip3 install pygame for python3